Menu

Espressioni lambda in C#: sintassi, Func e Action, closure e LINQ

Come funzionano le espressioni lambda in C#: la sintassi => in tutte le sue forme, memorizzare le lambda in Func e Action, passarle ai metodi e a LINQ, le lambda con blocco di istruzioni, le closure sulle variabili catturate e il bug di cattura nel ciclo for che foreach non ha.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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 lambda static a cui è vietato catturare variabili.
  • C# 10: un "tipo naturale", così var square = (int x) => x * x; deduce Func<int, int>. Prima di allora, var f = x => x * 2; fallisce; anche in C# 10 fallisce ancora senza il tipo del parametro, con CS8917 The 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;.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA