Un'espressione lambda è una piccola funzione anonima scritta direttamente nel codice con l'operatore => ("va in"). I parametri stanno a sinistra, il risultato a destra:
Output:
60.0
12
ORDER SHIPPED!
96.0, 180.0
Una lambda non ha un tipo proprio. Diventa un delegate (un oggetto che punta a una funzione) del tipo delegate che il contesto si aspetta: qui Func<decimal, decimal>, un Predicate<T> per List.FindAll, un EventHandler per un evento. I delegate Func restituiscono un valore e i delegate Action no; la pagina su Func e Action elenca l'intera famiglia.
Forme della sintassi
x => x * x // one parameter: parentheses optional
(x, y) => x + y // several parameters
() => DateTime.Now.Year // no parameters
(int x, string s) => s.Length > x // explicit parameter types
order => // statement lambda: a block with return
{
decimal subtotal = order.Qty * order.Price;
return subtotal > 100m ? subtotal * 0.9m : subtotal;
}
Una lambda di espressione ha una sola espressione dopo =>, e il suo valore è il risultato. Una lambda con blocco di istruzioni ha un blocco tra graffe, che può contenere qualsiasi istruzione e deve fare return di un valore su ogni percorso se il delegate restituisce qualcosa (altrimenti CS1643, Not all code paths return a value in lambda expression).
Le versioni più recenti hanno aggiunto altre forme, tutte facoltative:
- C# 9: parametri discard, così più parametri inutilizzati possono essere tutti
_((_, _) => Save()), e lambdastatica cui è vietato catturare variabili. - C# 10: un "tipo naturale", così
var square = (int x) => x * x;deduceFunc<int, int>. Prima di allora,var f = x => x * 2;fallisce; anche in C# 10 fallisce ancora senza il tipo del parametro, con CS8917The delegate type could not be inferred. - C# 12: valori predefiniti dei parametri,
(int x, int y = 1) => x + y.
Passare lambda ai metodi
Qualsiasi metodo che accetta un parametro delegate accetta una lambda. È così che passi un comportamento, non solo dati:
Output:
report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf
Filter non sa cosa significhi "tenere"; lo decide chi chiama con una lambda. List.Sort funziona allo stesso modo: la lambda è un confronto che restituisce un valore negativo, zero o positivo, e Sort la chiama tutte le volte che serve.
Lambda in LINQ
LINQ è il posto in cui la maggior parte del codice C# incontra le lambda. Ogni operatore di query ne accetta una: Where una condizione, Select una proiezione, OrderBy una chiave, Sum un valore da sommare:
Output:
Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45
Le lambda non vengono eseguite dove sono scritte. LINQ le memorizza e le chiama quando la query viene enumerata da foreach, ToList() o Sum(): si chiama esecuzione differita. La pagina su LINQ tratta gli operatori.
Closure: variabili catturate
Una lambda può usare le variabili locali e i parametri del metodo in cui è scritta. Cattura la variabile, non il suo valore attuale, e la variabile continua a vivere quanto la lambda:
Output:
1
2
3
1
50.0
Ogni chiamata a MakeCounter crea un nuovo count, quindi i due contatori sono indipendenti. La lambda dello sconto ha letto rate quando è stata eseguita, non quando è stata scritta, quindi ha applicato il 50%. Il compilatore lo implementa spostando le variabili catturate in una classe nascosta che il metodo e la lambda condividono.
La trappola della cattura nei cicli
Catturare una variabile di ciclo è il punto in cui le closure fanno male. Un ciclo for ha una sola variabile per tutto il ciclo, quindi ogni lambda creata al suo interno condivide quella variabile:
Output:
3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach
Tutte e tre le lambda del primo ciclo sono state eseguite dopo la fine del ciclo, quando i valeva 3. Copiare i in una variabile dichiarata dentro il corpo dà a ogni lambda la sua. foreach lo fa correttamente da solo: da C# 5, la sua variabile di iterazione è una variabile nuova a ogni passaggio. Prima di C# 5 aveva lo stesso bug, ed è per questo che le risposte più vecchie online consigliano ancora la copia anche per foreach.
La stessa trappola vale per tutto ciò che viene eseguito più tardi: gestori di eventi, timer, Task.Run e query LINQ costruite in un ciclo ed enumerate dopo.
Lambda, metodi anonimi e funzioni locali
Le lambda hanno sostituito la sintassi dei metodi anonimi di C# 2, che compila ancora:
Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; }; // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0; // lambda
Per un helper usato solo dentro un metodo, C# 7.0 offre anche le funzioni locali, che hanno un nome, possono essere ricorsive e non allocano un delegate a meno che tu non le converta in uno. Usa una lambda quando passi un comportamento a qualcosa (LINQ, Sort, un evento); usa una funzione locale o un metodo privato quando lo chiami tu stesso.
Un'altra distinzione compare con i provider LINQ come Entity Framework: una lambda assegnata a Expression<Func<T, bool>> non viene compilata in codice ma in una struttura dati che descrive il codice, che il provider traduce in SQL. La sintassi è identica; è il tipo del parametro del metodo che chiami a decidere cosa ottieni.
Domande frequenti
Che cos'è un'espressione lambda in C#?
Una funzione anonima scritta con l'operatore =>: x => x * 2 accetta x e restituisce x * 2. Una lambda non ha un nome proprio; viene convertita in un tipo delegate come Func<int, int> o Action<string> e può essere memorizzata in una variabile, passata a un metodo o restituita da un metodo.
Come scrivo una lambda con più parametri o senza parametri?
Metti i parametri tra parentesi: (a, b) => a + b per due, () => DateTime.Now per nessuno. Solo un parametro singolo può fare a meno delle parentesi (x => x + 1). Puoi anche indicare i tipi esplicitamente: (int a, int b) => a + b.
Cos'è una closure in una lambda C#?
Una lambda che usa una variabile locale del metodo che la contiene cattura quella variabile, non una fotografia del suo valore. La variabile vive quanto la lambda, e se una delle due parti la modifica in seguito, l'altra vede la modifica. È ciò che fa funzionare contatori e callback, ed è ciò che causa il classico bug di cattura nel ciclo for.
Perché le lambda in un ciclo for vedono tutte lo stesso valore?
Un ciclo for ha una sola variabile di ciclo per tutto il ciclo, quindi ogni lambda creata al suo interno cattura lo stesso i e vede il suo valore finale quando viene eseguita più tardi. Copialo in una variabile locale dentro il corpo (int copy = i;) e cattura la copia. foreach non ha questo problema da C# 5, perché crea una variabile nuova a ogni iterazione.
Perché var f = x => x * 2 non compila?
Il compilatore non riesce a capire quale tipo delegate vuoi né di che tipo è x, quindi segnala CS8917 The delegate type could not be inferred (prima di C# 10, CS0815). Dichiara il tipo (Func<int, int> f = x => x * 2;) oppure, da C# 10 in poi, indica il tipo del parametro: var f = (int x) => x * 2;.