Lambda expressions
- #csharp
- #programmazione
- #lambda
- #linq
- #funzionale
In questa lezione
- 1. Introduzione
- 2. Sintassi
- 2.1 Expression lambda e statement lambda
- 2.2 Parametri, parentesi e tipi
- 3. Lambda e delegate
- 3.1 Func, Action, Predicate e delegate personalizzati
- 3.2 Lambda come callback
- 4. Closures
- 4.1 Captured variable trap nei loop
- 5. Delegate vs expression tree
- 6. Lambda con LINQ
- 6.1 Where, Select, OrderBy
- 6.2 GroupBy e Aggregate
- 6.3 Query syntax vs method syntax
- 6.4 Esempio con lambda annidate
- 7. Lambda ricorsive e local functions
- 8. Performance e static lambda
- 9. Pattern funzionali
- 9.1 Currying e partial application
- 9.2 Composizione e pipeline
- 10. Best practice
- 11. Quiz
- Leggi il codice: cosa stampa? dov’è il bug?
- 12. Esercizi
- 12.1 Esercizio
- 12.2 Esercizio
- 12.3 Esercizio
- 12.4 Esercizio
1. Introduzione
Le lambda expressions sono il modo più compatto con cui il C# moderno rappresenta una funzione anonima. Una lambda descrive un comportamento che può essere:
- assegnato a un delegate o passato come parametro;
- restituito da un metodo;
- trasformato in un expression tree;
- usato per costruire query LINQ.
L’idea viene dal lambda calculus di Alonzo Church (1936), dove una funzione si scrive ( parametro, corpo) ed è la base teorica della programmazione funzionale: le funzioni sono valori di prima classe, che si possono passare, restituire e comporre.
Nota
Le lambda sono arrivate con C# 3.0 (2007), insieme a LINQ, extension methods, anonymous types ed expression trees. Da allora il C# è un linguaggio ibrido: orientato agli oggetti, ma con forti strumenti funzionali.
2. Sintassi
La forma base è:
(parametri) => espressione
L’operatore => si legge “va in” oppure “produce”.
2.1 Expression lambda e statement lambda
Una expression lambda ha come corpo una singola espressione, il cui valore viene restituito implicitamente.
Una statement lambda usa un blocco { } e, se il delegate ha un tipo di ritorno, richiede return esplicito.
// expression lambda
Func<int, int> raddoppia = x => x * 2;
// statement lambda
Func<int, int, int> max = (a, b) =>
{
if (a > b)
return a;
return b;
};
Console.WriteLine(raddoppia(4)); // 8
Console.WriteLine(max(3, 7)); // 7
2.2 Parametri, parentesi e tipi
Action saluto = () => Console.WriteLine("ciao"); // nessun parametro: () obbligatorie
Func<int, int> quadrato = x => x * x; // un parametro: parentesi opzionali
Func<int, int, int> prodotto = (x, y) => x * y; // più parametri: parentesi obbligatorie
Func<int, int, int> somma = (int x, int y) => x + y; // tipi espliciti
Func<int, int> assoluto = (int x) => x < 0 ? -x : x;
saluto(); // ciao
Console.WriteLine(prodotto(3, 4)); // 12
Console.WriteLine(assoluto(-9)); // 9
Una lambda non ha un tipo da sola: il tipo viene inferito dal contesto di destinazione.
In Func<int, bool> pari = x => x % 2 == 0; è il compilatore a dedurre che x è un int e che il risultato deve essere bool.
Suggerimento
I tipi espliciti dei parametri sono utili quando l’inferenza non è immediata o ci sono overload ambigui. Nei casi semplici lasciali inferire: il codice resta più pulito.
3. Lambda e delegate
Una lambda viene quasi sempre convertita in un delegate (o in un expression tree, vedi sezione 5). È una shorthand sintattica del vecchio delegate anonimo:
Func<int, int> doppio1 = delegate (int x) { return x * 2; };
Func<int, int> doppio2 = x => x * 2; // equivalente, più compatto
3.1 Func, Action, Predicate e delegate personalizzati
Action<T...>: nessun valore di ritorno;Func<T..., TResult>: restituisce un valore (l’ultimo tipo generico è il ritorno);Predicate<T>: restituiscebool.
Action<string> log = messaggio => Console.WriteLine($"LOG: {messaggio}");
Func<decimal, decimal, decimal> sconto = (prezzo, perc) => prezzo * (1 - perc);
Predicate<int> positivo = n => n > 0;
log("Operazione completata"); // LOG: Operazione completata
Console.WriteLine(sconto(100m, 0.15m)); // 85,00
Console.WriteLine(positivo(42)); // True
Puoi assegnare una lambda anche a un delegate dichiarato da te, utile quando vuoi dare un nome semantico alla firma:
public delegate decimal CalcoloPrezzo(decimal imponibile, decimal iva);
CalcoloPrezzo totale = (imponibile, iva) => imponibile + iva;
Console.WriteLine(totale(100m, 22m)); // 122
3.2 Lambda come callback
Molti metodi ricevono un comportamento come parametro: il chiamante lo definisce inline con una lambda.
static void EseguiOperazione(int valore, Action<int> callback)
{
Console.WriteLine("Elaborazione...");
callback(valore);
}
EseguiOperazione(10, x => Console.WriteLine($"Risultato: {x * 3}"));
// Elaborazione...
// Risultato: 30
Un esempio più realistico: lo stesso metodo di ordinamento, con criteri diversi passati come lambda.
var parole = new List<string> { "banana", "uva", "mela", "albicocca" };
parole.Sort((a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", parole)); // uva, mela, banana, albicocca
parole.Sort((a, b) => string.Compare(a, b, StringComparison.Ordinal));
Console.WriteLine(string.Join(", ", parole)); // albicocca, banana, mela, uva
4. Closures
Una closure si verifica quando una lambda cattura variabili del suo outer scope:
int fattore = 3;
Func<int, int> moltiplica = x => x * fattore;
Console.WriteLine(moltiplica(10)); // 30
Viene catturata la variabile, non il suo valore al momento della creazione: se la variabile cambia, la lambda vede il nuovo valore.
int contatore = 0;
Action incrementa = () => contatore++;
incrementa();
incrementa();
Console.WriteLine(contatore); // 2
fattore = 5;
Console.WriteLine(moltiplica(10)); // 50, non 30!
Nota
Internamente il compilatore genera una classe nascosta (display class) con un campo per ogni variabile catturata e un metodo con il corpo della lambda. Per questo una variabile catturata vive almeno quanto il delegate che la usa, anche dopo la fine del metodo in cui è stata dichiarata.
4.1 Captured variable trap nei loop
Il problema classico compare nei cicli for:
var azioni = new List<Action>();
for (int i = 0; i < 3; i++)
{
azioni.Add(() => Console.WriteLine(i));
}
foreach (var azione in azioni)
azione();
// Output: 3 3 3 (non 0 1 2)
Tutte le lambda catturano la stessa variabile i, che a fine ciclo vale 3.
La soluzione è una copia locale per ogni iterazione:
for (int i = 0; i < 3; i++)
{
int copia = i;
azioni.Add(() => Console.WriteLine(copia)); // 0 1 2
}
Attenzione
Da C# 5 il foreach crea una nuova variabile a ogni iterazione, quindi non ha più questo problema. Con for (e con variabili mutate a mano) la trappola esiste ancora: se vuoi “congelare” un valore, copialo in una variabile locale.
Le closure sono utilissime, ma possono anche tenere in vita oggetti grandi più del previsto e introdurre allocazioni aggiuntive (vedi sezione 8).
5. Delegate vs expression tree
La stessa lambda può diventare due cose molto diverse, a seconda del tipo di destinazione:
Func<Prodotto, bool> filtroInMemoria = p => p.Prezzo > 100;
Expression<Func<Prodotto, bool>> filtroTraducibile = p => p.Prezzo > 100;
Func<...>: codice eseguibile dal runtime .NET; tipico diIEnumerable<T>e LINQ to Objects.Expression<Func<...>>: una struttura dati ad albero che descrive il codice; un provider la legge e la traduce (SQL, API remote…). Tipico diIQueryable<T>ed EF Core.
Un expression tree si può ispezionare e, se serve, compilare in un delegate:
using System.Linq.Expressions;
Expression<Func<int, bool>> expr = x => x % 2 == 0;
Console.WriteLine(expr.Body); // ((x % 2) == 0)
Console.WriteLine(expr.NodeType); // Lambda
Func<int, bool> pari = expr.Compile();
Console.WriteLine(pari(4)); // True
Con EF Core, se db.Prodotti è un IQueryable<Prodotto>, la lambda non viene eseguita su ogni record ma tradotta in SQL:
var query = db.Prodotti.Where(p => p.Prezzo > 100);
// SELECT * FROM Prodotti WHERE Prezzo > 100
Attenzione
Solo le expression lambda possono diventare expression tree: una statement lambda (con blocco { }) non compila se assegnata a Expression<...>. Inoltre, se dentro la lambda chiami metodi C# che il provider non sa tradurre, EF Core lancia un’eccezione o sposta il lavoro lato client, con impatto sulle prestazioni.
6. Lambda con LINQ
LINQ è il terreno naturale delle lambda. Useremo questa collezione:
public class Studente
{
public string Nome { get; set; } = "";
public int Eta { get; set; }
public string Corso { get; set; } = "";
public List<int> Voti { get; set; } = new();
}
var studenti = new List<Studente>
{
new() { Nome = "Anna", Eta = 21, Corso = "Informatica", Voti = new() { 28, 30, 27 } },
new() { Nome = "Luca", Eta = 19, Corso = "Matematica", Voti = new() { 18, 24, 21 } },
new() { Nome = "Marta", Eta = 23, Corso = "Informatica", Voti = new() { 30, 29, 30 } },
new() { Nome = "Paolo", Eta = 22, Corso = "Fisica", Voti = new() { 25, 26, 24 } }
};
6.1 Where, Select, OrderBy
Le lambda descrivono l’intera pipeline: filtro, proiezione, ordinamento.
var risultati = studenti
.Where(s => s.Corso == "Informatica")
.Select(s => new { s.Nome, Media = s.Voti.Average() })
.OrderByDescending(s => s.Media)
.ToList();
foreach (var r in risultati)
Console.WriteLine($"{r.Nome}: {r.Media:F1}");
// Marta: 29,7
// Anna: 28,3
6.2 GroupBy e Aggregate
La lambda di GroupBy sceglie la chiave di raggruppamento:
var perCorso = studenti
.GroupBy(s => s.Corso)
.Select(g => new
{
Corso = g.Key,
Numero = g.Count(),
Media = g.SelectMany(s => s.Voti).Average()
});
foreach (var g in perCorso)
Console.WriteLine($"{g.Corso}: {g.Numero} studenti, media {g.Media:F1}");
// Informatica: 2 studenti, media 29,0
// Matematica: 1 studenti, media 21,0
// Fisica: 1 studenti, media 25,0
Aggregate riceve l’accumulatore corrente e l’elemento corrente, e produce il nuovo accumulatore:
int sommaVotiAnna = studenti
.First(s => s.Nome == "Anna")
.Voti
.Aggregate(0, (acc, voto) => acc + voto);
Console.WriteLine(sommaVotiAnna); // 85
6.3 Query syntax vs method syntax
La query syntax viene tradotta dal compilatore in chiamate di metodo con lambda:
var q1 =
from s in studenti
where s.Eta >= 21
orderby s.Nome
select s.Nome;
// equivalente
var q2 = studenti
.Where(s => s.Eta >= 21)
.OrderBy(s => s.Nome)
.Select(s => s.Nome);
Suggerimento
La query syntax è spesso più leggibile con join multipli; la method syntax è più uniforme e copre tutti gli operatori (Count, Aggregate, Take…). Nella pratica si usa soprattutto la method syntax.
6.4 Esempio con lambda annidate
public record RigaOrdine(string Prodotto, int Quantita, decimal PrezzoUnitario);
public record Ordine(string Cliente, string Citta, List<RigaOrdine> Righe);
var ordini = new List<Ordine>
{
new("Alfa Srl", "Milano", new() { new("Monitor", 2, 180m), new("Mouse", 5, 20m) }),
new("Beta Spa", "Roma", new() { new("Notebook", 1, 1200m) }),
new("Gamma Srl", "Milano", new() { new("Monitor", 1, 220m), new("Tastiera", 3, 45m) })
};
var report = ordini
.Where(o => o.Citta == "Milano")
.Select(o => new
{
o.Cliente,
Totale = o.Righe.Sum(r => r.Quantita * r.PrezzoUnitario)
})
.OrderByDescending(x => x.Totale);
foreach (var r in report)
Console.WriteLine($"{r.Cliente}: {r.Totale}");
// Alfa Srl: 460
// Gamma Srl: 355
Qui una lambda (r => r.Quantita * r.PrezzoUnitario) vive dentro un’altra lambda (o => new { ... }).
7. Lambda ricorsive e local functions
Una lambda non ha un nome, quindi non può richiamare sé stessa direttamente: serve una variabile che contenga il delegate.
Func<int, int>? fattoriale = null;
fattoriale = n => n <= 1 ? 1 : n * fattoriale!(n - 1);
Console.WriteLine(fattoriale(5)); // 120
Funziona, ma la local function è molto più naturale:
int Calcola(int valore)
{
return Fattoriale(valore);
static int Fattoriale(int n) => n <= 1 ? 1 : n * Fattoriale(n - 1);
}
Quando scegliere l’una o l’altra:
- Local function: ricorsione, parametri
ref/out/in, logica locale strutturata con un nome leggibile. - Lambda: callback inline, query LINQ, codice breve vicino al punto d’uso.
var nomi = new[] { "anna", "luca", "marta" };
var maiuscoli = nomi.Select(n => n.ToUpperInvariant()).ToList(); // ANNA, LUCA, MARTA
Suggerimento
Se una lambda supera 4-5 righe o contiene logica di business, estraila in una local function o in un metodo con un nome: la query LINQ torna leggibile e la logica diventa testabile.
8. Performance e static lambda
Una lambda senza capture (x => x * 2) è praticamente gratuita: il delegate viene creato una volta e riusato.
Una lambda con capture richiede in genere una display class e un nuovo delegate ogni volta che la closure viene creata (non a ogni invocazione): più allocazioni e più pressione sul GC.
Con le static lambda (C# 9) dichiari che la lambda non deve catturare nulla, e il compilatore lo verifica:
var numeri = Enumerable.Range(1, 5);
var quadrati = numeri.Select(static n => n * n).ToList(); // OK
int offset = 10;
// var valori = numeri.Select(static n => n + offset).ToList();
// errore CS8820: una lambda static non può fare riferimento a 'offset'
Nota
Nel codice applicativo il costo delle closure è quasi sempre trascurabile. Diventa rilevante in librerie, hot path ed elaborazioni massive: lì static è un modo semplice per evitare catture accidentali.
9. Pattern funzionali
9.1 Currying e partial application
Il currying trasforma una funzione con più argomenti in una catena di funzioni a un argomento. La partial application fissa alcuni argomenti per ottenere una funzione più specializzata.
// currying
Func<int, Func<int, int>> sommaCurried = x => y => x + y;
var aggiungi10 = sommaCurried(10);
Console.WriteLine(aggiungi10(5)); // 15
// partial application
Func<decimal, decimal, decimal> applicaSconto = (prezzo, perc) => prezzo * (1 - perc);
Func<decimal, decimal> scontoBlackFriday = prezzo => applicaSconto(prezzo, 0.30m);
Console.WriteLine(scontoBlackFriday(200m)); // 140,00
9.2 Composizione e pipeline
La composizione si implementa restituendo una lambda:
static Func<TIn, TOut> Compose<TIn, TMid, TOut>(Func<TMid, TOut> g, Func<TIn, TMid> f)
=> x => g(f(x));
Func<int, int> doppio = x => x * 2;
Func<int, string> formatta = x => $"Valore: {x}";
var pipeline = Compose(formatta, doppio);
Console.WriteLine(pipeline(5)); // Valore: 10
Il C# non ha un pipeline operator, ma un extension method lo simula bene:
public static class FunctionalExtensions
{
public static TResult Pipe<TSource, TResult>(this TSource value, Func<TSource, TResult> func)
=> func(value);
}
string output = 5
.Pipe(x => x * 2)
.Pipe(x => x + 1)
.Pipe(x => $"Risultato finale: {x}");
Console.WriteLine(output); // Risultato finale: 11
10. Best practice
- tieni le lambda brevi e focalizzate; se il corpo cresce, estrai in metodo o local function;
- usa nomi espliciti per le variabili che contengono delegate;
- nei loop
for, crea una copia locale del valore da catturare; - usa
staticper impedire catture accidentali nei punti critici; - con
IQueryable, scrivi lambda traducibili dal provider; - preferisci
Func,Action,Predicate; usa delegate personalizzati quando la firma ha un significato di dominio.
Suggerimento
In ogni punto del codice chiediti che ruolo ha la lambda: codice eseguibile (delegate), struttura dati (expression tree) o closure che porta con sé parte dell’ambiente esterno. Capirlo evita la maggior parte degli errori.
11. Quiz
Mettiti alla prova
0/9 risposte
Quale sintassi rappresenta correttamente una lambda expression in C#?
Quale forma è una statement lambda?
Una lambda in C# viene tipicamente convertita in:
Cosa significa che una lambda crea una closure?
Qual è il problema classico quando si cattura la variabile di un ciclo for?
Qual è la differenza chiave tra Func<T, bool> ed Expression<Func<T, bool>>?
Perché EF Core usa spesso Expression<Func<T, TResult>>?
Quando una local function è spesso preferibile a una lambda?
Cosa garantisce una static lambda?
Leggi il codice: cosa stampa? dov’è il bug?
Leggi il codice
0/4 risposte
Cosa stampa questo codice?
Cosa stampa?Func<int, int> doppio = x => x * 2; Func<int, int> piuUno = x => x + 1; Console.WriteLine(doppio(piuUno(3))); Console.WriteLine(piuUno(doppio(3)));Closure in un ciclo
Cosa stampa?for: cosa stampa?var azioni = new List<Action>(); for (int i = 0; i < 3; i++) azioni.Add(() => Console.Write(i + " ")); foreach (var azione in azioni) azione();LINQ e esecuzione differita: cosa stampa?
var lista = new List<int> { 1, 2, 3 }; var query = lista.Where(x => x > 1); lista.Add(4); Console.WriteLine(query.Count());Vogliamo il quadrato dei numeri pari, ma il risultato è sbagliato. Clicca la riga con il bug.
Trova il bugClicca la riga che contiene l'errore.
var numeri = new List<int> { 1, 2, 3, 4, 5, 6 }; var quadratiPari = numeri .Where(n => n % 2 == 1) .Select(n => n * n); Console.WriteLine(string.Join(", ", quadratiPari)); // atteso: 4, 16, 36
12. Esercizi
12.1 Esercizio
Scenario: Stai sviluppando un pannello amministrativo per un e-commerce e vuoi permettere agli operatori di applicare filtri dinamici ai prodotti.
Consegna:
- Crea una classe
Prodottocon proprietàNome,Categoria,Prezzo,Disponibile. - Crea una lista di almeno 8 prodotti.
- Implementa un metodo
FiltraProdotti(List<Prodotto> prodotti, Func<Prodotto, bool> filtro). - Usa il metodo con tre lambda diverse:
- prodotti disponibili;
- prodotti con prezzo maggiore di 100;
- prodotti della categoria “Monitor” con prezzo inferiore a 300.
- Stampa il risultato di ogni filtro.
Obiettivo didattico: usare le lambda come callback riutilizzabili e capire il ruolo di Func<T, bool> nei filtri dinamici.
12.2 Esercizio
Scenario: Vuoi costruire un piccolo modulo analytics per analizzare gli ordini di un gestionale.
Consegna:
- Crea le classi (o record)
OrdineeRigaOrdine. - Ogni ordine deve avere
Cliente,Cittae una lista di righe conProdotto,Quantita,PrezzoUnitario. - Usa LINQ con lambda per ottenere:
- il totale di ogni ordine;
- gli ordini della città “Milano”;
- il fatturato totale per cliente;
- il prodotto più venduto in quantità.
- Riscrivi almeno una delle query usando query syntax e confrontala con la method syntax.
Obiettivo didattico: consolidare Where, Select, GroupBy, SelectMany e Aggregate in uno scenario realistico.
12.3 Esercizio
Scenario: Stai implementando un motore di regole per assegnare sconti a clienti premium.
Consegna:
- Usa
Func<decimal, decimal>per rappresentare una trasformazione di prezzo. - Implementa tre lambda:
- sconto del 10%;
- sconto del 20%;
- sconto del 10% seguito da IVA al 22%.
- Crea una utility
Composeper comporre due funzioni. - Mostra come ottenere una pipeline di trasformazioni da applicare a un prezzo iniziale.
- Indica nel codice dove stai facendo currying, partial application o composizione.
Obiettivo didattico: applicare pattern funzionali reali con lambda annidate e composizione di funzioni.
12.4 Esercizio
Scenario: Hai una coda di job da eseguire in background e vuoi registrare callback diverse per successo ed errore.
Consegna:
- Crea una classe
JobRunnercon un metodoEsegui(string nomeJob, Action<string> onSuccess, Action<string> onError). - Simula l’esecuzione di più job, facendo fallire almeno uno.
- Usa lambda inline come callback per:
- scrivere su console;
- incrementare contatori esterni;
- salvare i job falliti in una lista.
- Riproduci volontariamente un caso di closure in un ciclo
fore osserva il problema. - Correggi il bug introducendo una variabile temporanea locale nel loop.
Obiettivo didattico: comprendere callback, closure, lifetime delle variabili catturate e bug classici nei loop.