Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalLe lambda expressions sono il modo più compatto e potente con cui il C# moderno rappresenta una funzione anonima. Quando scrivi una lambda, stai descrivendo un comportamento che può essere:
Dal punto di vista storico, l’idea non nasce in C#, ma nella logica matematica.
Nel 1936 Alonzo Church introdusse il lambda calculus, un formalismo matematico per descrivere funzioni, applicazione di funzioni e computazione.
Le due idee fondamentali sono:
La forma classica è:
dove:
Applicare la funzione a un valore significa sostituire il parametro con l’argomento:
Esempio:
Questo formalismo è la base teorica di gran parte della programmazione funzionale.
In programmazione funzionale una funzione è trattata come valore di prima classe:
Le lambda expressions in C# portano proprio questa idea nel linguaggio object-oriented di .NET.
Le lambda expressions sono state introdotte con C# 3.0 nel 2007, insieme a:
Da quel momento il C# ha adottato un modello ibrido: orientato agli oggetti, ma con forti strumenti funzionali.
La sintassi base di una lambda è:
(parametri) => espressione
L’operatore => si legge spesso come “va in” oppure “produce”.
Una expression lambda ha come corpo una singola espressione:
Func<int, int> raddoppia = x => x * 2;
Func<int, int, int> somma = (x, y) => x + y;
Se il corpo è una singola espressione, il valore viene restituito implicitamente.
Una statement lambda usa un blocco di istruzioni:
Action<string> saluta = nome =>
{
string messaggio = $"Ciao {nome}";
Console.WriteLine(messaggio);
};
In questo caso, se il delegate ha un tipo di ritorno, devi usare return esplicito:
Func<int, int, int> max = (a, b) =>
{
if (a > b)
return a;
return b;
};
Con un solo parametro le parentesi possono essere omesse:
Func<int, int> quadrato = x => x * x;
Senza parametri servono sempre le parentesi vuote:
Action saluto = () => Console.WriteLine("ciao");
Con più parametri le parentesi sono obbligatorie:
Func<int, int, int> prodotto = (x, y) => x * y;
Una lambda non ha un tipo “da sola”. Il tipo viene inferito dal contesto di destinazione:
Func<int, bool> pari = x => x % 2 == 0;
Predicate<string> vuota = s => string.IsNullOrWhiteSpace(s);
Qui è il compilatore che deduce:
x è un int;bool;Func<int, bool> o Predicate<string>.Se vuoi puoi dichiarare esplicitamente i tipi:
Func<int, int, int> somma = (int x, int y) => x + y;
Questo è utile quando:
Action stampa = () => Console.WriteLine("Nessun parametro");
Func<int, int> doppio = x => x * 2;
Func<int, int, int> differenza = (x, y) => x - y;
Func<int, int> assoluto = (int x) => x < 0 ? -x : x;
stampa();
Console.WriteLine(doppio(5));
Console.WriteLine(differenza(10, 3));
Console.WriteLine(assoluto(-9));
Una lambda in C# non vive nel vuoto: viene quasi sempre convertita in un delegate o in un expression tree.
Queste due forme sono concettualmente equivalenti:
Func<int, int> doppio1 = delegate (int x)
{
return x * 2;
};
Func<int, int> doppio2 = x => x * 2;
La lambda è quindi una shorthand sintattica per rappresentare un delegate anonimo in modo più compatto.
Func, Action e PredicateLe lambda sono usate continuamente con i delegati generici predefiniti:
Action<string> log = messaggio => Console.WriteLine($"LOG: {messaggio}");
Func<decimal, decimal, decimal> sconto = (prezzo, percentuale) => prezzo * (1 - percentuale);
Predicate<int> positivo = n => n > 0;
log("Operazione completata");
Console.WriteLine(sconto(100m, 0.15m));
Console.WriteLine(positivo(42));
Riepilogo:
Action<T...>: nessun valore di ritorno;Func<T..., TResult>: restituisce un valore;Predicate<T>: restituisce bool.Una lambda può essere assegnata anche a un delegato dichiarato da te:
public delegate decimal CalcoloPrezzo(decimal imponibile, decimal iva);
CalcoloPrezzo totale = (imponibile, iva) => imponibile + iva;
decimal risultato = totale(100m, 22m);
Console.WriteLine(risultato);
Questo è utile quando vuoi dare un nome semantico alla firma.
Molti metodi ricevono lambda come parametri:
public static void EseguiOperazione(int valore, Action<int> callback)
{
Console.WriteLine("Elaborazione...");
callback(valore);
}
EseguiOperazione(10, x => Console.WriteLine($"Risultato: {x * 3}"));
Qui la lambda è una callback inline: il chiamante definisce direttamente il comportamento da eseguire.
La stessa lambda può essere compatibile con più target, ma il contesto decide la conversione:
Func<int, bool> filtro = x => x > 10;
Expression<Func<int, bool>> filtroEspressione = x => x > 10;
La sintassi è uguale, ma il risultato semantico è diverso:
Una closure si verifica quando una lambda cattura variabili definite nel suo outer scope.
int fattore = 3;
Func<int, int> moltiplica = x => x * fattore;
Console.WriteLine(moltiplica(10)); // 30
La lambda non usa solo il parametro x, ma anche la variabile esterna fattore.
Questo significa che la lambda “chiude sopra” il contesto in cui è nata.
Quando una lambda cattura variabili, il compilatore genera una classe nascosta, spesso chiamata concettualmente display class, con:
Esempio concettuale:
int fattore = 3;
Func<int, int> moltiplica = x => x * fattore;
viene trasformato in qualcosa di simile a:
private sealed class DisplayClass
{
public int fattore;
public int Moltiplica(int x)
{
return x * fattore;
}
}
DisplayClass closure = new DisplayClass();
closure.fattore = 3;
Func<int, int> moltiplica = closure.Moltiplica;
flowchart TD
A[Codice sorgente con lambda] --> B[Compilatore]
B --> C[Display class generata]
C --> D[Campo fattore]
C --> E[Metodo Moltiplica]
E --> F[Delegate Func<int,int>]
Uno degli aspetti più importanti è questo: di solito viene catturata la variabile, non il suo valore al momento della creazione.
int contatore = 0;
Action incrementa = () => contatore++;
incrementa();
incrementa();
Console.WriteLine(contatore); // 2
La lambda sta lavorando sulla stessa variabile contatore.
Il problema classico compare nei cicli:
var azioni = new List<Action>();
for (int i = 0; i < 3; i++)
{
azioni.Add(() => Console.WriteLine(i));
}
foreach (var azione in azioni)
{
azione();
}
Molti principianti si aspettano:
0
1
2
ma l’output tipico è:
3
3
3
perché tutte le lambda catturano la stessa variabile i, che al termine del ciclo vale 3.
La soluzione è creare una nuova variabile locale per ogni iterazione:
var azioni = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copia = i;
azioni.Add(() => Console.WriteLine(copia));
}
Così ogni lambda cattura una variabile distinta.
for e foreachStoricamente il problema era molto noto con foreach.
Le versioni moderne di C# hanno corretto il comportamento di foreach, ma con for la regola resta concettualmente importante:
Una variabile locale normale vive sullo stack del metodo. Una variabile catturata, invece, deve sopravvivere finché la lambda esiste.
In termini concettuali:
Questo significa che la closure può estendere la vita di oggetti che altrimenti sarebbero stati rilasciati prima.
Le closure sono utilissime, ma possono introdurre:
Non tutte le lambda sono equivalenti. La differenza più importante è tra expression lambda e statement lambda.
Una expression lambda ha un solo nodo espressivo:
Func<int, bool> pari = x => x % 2 == 0;
Questa forma può essere convertita sia in un delegate sia, in molti casi, in un expression tree:
Expression<Func<int, bool>> expr = x => x % 2 == 0;
Un expression tree non esegue direttamente il codice: lo rappresenta come struttura ad albero.
graph TD
A[x => x.Price > 100] --> B[Expression Tree]
B --> C[Provider LINQ]
C --> D[Traduzione SQL o altra query]
Una statement lambda usa un blocco:
Func<int, bool> test = x =>
{
Console.WriteLine($"Controllo {x}");
return x % 2 == 0;
};
Questa forma è perfetta per callback e logica in memoria, ma non può essere convertita nello stesso modo a un expression tree traducibile da provider come Entity Framework Core.
Func<T, TResult> vs Expression<Func<T, TResult>>Questa è una distinzione fondamentale:
Func<Prodotto, bool> filtroInMemoria = p => p.Prezzo > 100;
Expression<Func<Prodotto, bool>> filtroTraducibile = p => p.Prezzo > 100;
Nel primo caso:
IEnumerable<T> e LINQ to Objects.Nel secondo caso:
IQueryable<T> ed EF Core.Con EF Core:
var query = db.Prodotti.Where(p => p.Prezzo > 100);
se Prodotti è IQueryable<Prodotto>, la lambda non viene eseguita subito su ogni record.
Viene prima analizzata e tradotta in qualcosa di simile a:
SELECT *
FROM Prodotti
WHERE Prezzo > 100
Se usassi una lambda non traducibile, per esempio con logica arbitraria o statement body, il provider potrebbe:
LINQ è il terreno naturale delle lambda expressions.
Where, Select e chainingSupponiamo di avere:
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 } }
};
var risultati = studenti
.Where(s => s.Corso == "Informatica")
.Select(s => new
{
s.Nome,
Media = s.Voti.Average()
})
.OrderByDescending(s => s.Media)
.ToList();
Qui le lambda descrivono l’intera pipeline:
Where;Select;OrderByDescending.Costo concettuale in memoria:
mentre un ordinamento aggiunge tipicamente:
GroupByvar gruppiPerCorso = studenti
.GroupBy(s => s.Corso)
.Select(g => new
{
Corso = g.Key,
NumeroStudenti = g.Count(),
MediaCorso = g.SelectMany(s => s.Voti).Average()
});
La lambda di GroupBy sceglie la chiave di raggruppamento.
Joinvar docenti = new[]
{
new { Corso = "Informatica", Docente = "Prof.ssa Rinaldi" },
new { Corso = "Matematica", Docente = "Prof. Greco" },
new { Corso = "Fisica", Docente = "Prof. Neri" }
};
var piano = studenti.Join(
docenti,
studente => studente.Corso,
docente => docente.Corso,
(studente, docente) => new
{
studente.Nome,
studente.Corso,
docente.Docente
});
Le lambda del Join definiscono:
AggregateAggregate è uno dei metodi più “funzionali” di LINQ:
int sommaVotiAnna = studenti
.First(s => s.Nome == "Anna")
.Voti
.Aggregate(0, (acc, voto) => acc + voto);
Qui la lambda riceve:
La query syntax:
var query =
from s in studenti
where s.Eta >= 21
orderby s.Nome
select s.Nome;
viene tradotta dal compilatore in method syntax:
var query = studenti
.Where(s => s.Eta >= 21)
.OrderBy(s => s.Nome)
.Select(s => s.Nome);
La query syntax è spesso più leggibile in query complesse con join multipli. La method syntax è più uniforme e mette in primo piano le lambda.
public class Ordine
{
public int Id { get; set; }
public string Cliente { get; set; } = "";
public string Citta { get; set; } = "";
public List<RigaOrdine> Righe { get; set; } = new();
}
public class RigaOrdine
{
public string Prodotto { get; set; } = "";
public int Quantita { get; set; }
public decimal PrezzoUnitario { get; set; }
}
var ordini = new List<Ordine>
{
new()
{
Id = 1,
Cliente = "Alfa Srl",
Citta = "Milano",
Righe = new()
{
new() { Prodotto = "Monitor", Quantita = 2, PrezzoUnitario = 180m },
new() { Prodotto = "Mouse", Quantita = 5, PrezzoUnitario = 20m }
}
},
new()
{
Id = 2,
Cliente = "Beta Spa",
Citta = "Roma",
Righe = new()
{
new() { Prodotto = "Notebook", Quantita = 1, PrezzoUnitario = 1200m },
new() { Prodotto = "Dock", Quantita = 2, PrezzoUnitario = 90m }
}
},
new()
{
Id = 3,
Cliente = "Gamma Srl",
Citta = "Milano",
Righe = new()
{
new() { Prodotto = "Monitor", Quantita = 1, PrezzoUnitario = 220m },
new() { Prodotto = "Tastiera", Quantita = 3, PrezzoUnitario = 45m }
}
}
};
var report = ordini
.Where(o => o.Citta == "Milano")
.Select(o => new
{
o.Cliente,
Totale = o.Righe.Sum(r => r.Quantita * r.PrezzoUnitario),
Prodotti = o.Righe.Select(r => r.Prodotto).ToList()
})
.OrderByDescending(x => x.Totale)
.ToList();
Questo esempio mostra lambda annidate in:
Where;Select;Sum;Select interno;OrderByDescending.Le lambda in C# non hanno un nome intrinseco come i metodi. Per questo non puoi scrivere una ricorsione diretta “naturale” nel corpo senza un appoggio esterno.
Questa forma non è praticabile come un metodo nominato:
// concettualmente desiderato, ma non esiste un nome interno della lambda
// n => n <= 1 ? 1 : n * ???(n - 1)
Serve una variabile locale che contenga il delegate.
Func<int, int>? fattoriale = null;
fattoriale = n => n <= 1 ? 1 : n * fattoriale!(n - 1);
Console.WriteLine(fattoriale(5)); // 120
Oppure con una firma più esplicita:
Func<int, long>? fibonacci = null;
fibonacci = n =>
{
if (n <= 1)
return n;
return fibonacci!(n - 1) + fibonacci!(n - 2);
};
Funziona, ma non è sempre la soluzione più leggibile.
Lo stesso fattoriale con una local function:
static int Fattoriale(int n)
{
if (n <= 1)
return 1;
return n * Fattoriale(n - 1);
}
oppure dentro un metodo:
int Calcola(int valore)
{
static int Fattoriale(int n)
{
if (n <= 1)
return 1;
return n * Fattoriale(n - 1);
}
return Fattoriale(valore);
}
Per ricorsione vera, la local function di solito è più naturale.
Lambda e local functions non sono intercambiabili in tutto.
Una local function è spesso migliore quando:
ref, out o in;static local function.Esempio con ref:
void Incrementa(ref int x)
{
x++;
}
Una lambda non può esporre la stessa ergonomia in tutti questi scenari.
La lambda è ideale quando:
var nomi = new[] { "anna", "luca", "marta" };
var maiuscoli = nomi.Select(n => n.ToUpperInvariant()).ToList();
static local functionUna static local function non può catturare variabili esterne.
Questo porta due vantaggi:
int[] dati = [1, 2, 3, 4];
int soglia = 2;
var filtrati = dati.Where(static x => x > 2).ToArray();
La stessa idea vale per le static lambda introdotte in C# 9:
Func<int, int> triplo = static x => x * 3;
Se provi a catturare variabili esterne, il compilatore segnala errore.
Le lambda sono comode, ma non sono sempre “gratis”.
Se una lambda non cattura variabili esterne:
Func<int, int> doppio = x => x * 2;
il compilatore/runtime può spesso ottimizzare la situazione e riusare istanze delegate già create.
Costo concettuale:
ovvero nessuna nuova allocazione ad ogni chiamata del delegate.
Se invece catturi variabili esterne:
int fattore = 2;
Func<int, int> moltiplica = x => x * fattore;
in genere servono:
Costo concettuale:
per ogni creazione della closure, non per ogni singola invocazione.
Confronto tipico:
Func<int, int> senzaCapture = static x => x * 2;
int fattore = 2;
Func<int, int> conCapture = x => x * fattore;
In un benchmark reale, la seconda forma può mostrare:
Questo non significa “non usare mai closure”, ma usarle consapevolmente.
La static lambda serve proprio a impedire catture accidentali:
var numeri = Enumerable.Range(1, 5);
var quadrati = numeri.Select(static n => n * n).ToList();
Se scrivessi:
int offset = 10;
var valori = numeri.Select(static n => n + offset).ToList();
il compilatore lo rifiuterebbe, perché una lambda static non può catturare offset.
Le lambda sono quasi sempre perfette nel codice applicativo. L’attenzione alle allocazioni diventa importante soprattutto in:
Le lambda permettono di applicare vari pattern funzionali anche in C#.
Il currying trasforma una funzione con più argomenti in una catena di funzioni con un argomento alla volta.
Func<int, Func<int, int>> sommaCurried = x => y => x + y;
var aggiungi10 = sommaCurried(10);
Console.WriteLine(aggiungi10(5)); // 15
In notazione teorica:
può essere trasformata in:
La partial application consiste nel fissare alcuni argomenti per ottenere una funzione più specializzata.
Func<decimal, decimal, decimal> applicaSconto = (prezzo, percentuale) => prezzo * (1 - percentuale);
Func<decimal, decimal> scontoBlackFriday = prezzo => applicaSconto(prezzo, 0.30m);
Console.WriteLine(scontoBlackFriday(200m)); // 140
Una utility di composizione:
static Func<TInput, TOutput> Compose<TInput, TMiddle, TOutput>(
Func<TMiddle, TOutput> g,
Func<TInput, TMiddle> f)
{
return 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
Formalmente:
Il C# non ha un vero pipeline operator come altri linguaggi, ma puoi simulare il pattern:
public static class FunctionalExtensions
{
public static TResult Pipe<TSource, TResult>(
this TSource value,
Func<TSource, TResult> func)
{
return func(value);
}
}
string output = 5
.Pipe(x => x * 2)
.Pipe(x => x + 1)
.Pipe(x => $"Risultato finale: {x}");
Console.WriteLine(output);
Questa tecnica rende visibile il flusso trasformativo dei dati.
Le lambda sono uno strumento molto espressivo, ma possono rendere il codice peggiore se usate male.
IQueryable;static quando vuoi impedire capture accidentali;IQueryable, scrivi lambda traducibili;Func, Action, Predicate) quando la firma è banale;Una lambda in C# può essere vista come uno di questi tre ruoli:
Capire quale dei tre stai usando, in ogni punto del codice, è la chiave per non sbagliare.
Da quale formalismo matematico deriva il concetto moderno di lambda expression?
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?
Scenario: Stai sviluppando un pannello amministrativo per un e-commerce e vuoi permettere agli operatori di applicare filtri dinamici ai prodotti.
Consegna:
Prodotto con proprietà Nome, Categoria, Prezzo, Disponibile.FiltraProdotti(List<Prodotto> prodotti, Func<Prodotto, bool> filtro).Obiettivo didattico: usare le lambda come callback riutilizzabili e capire il ruolo di Func<T, bool> nei filtri dinamici.
Scenario: Vuoi costruire un piccolo modulo analytics per analizzare gli ordini di un gestionale.
Consegna:
Ordine e RigaOrdine.Cliente, Citta e una lista di righe con Prodotto, Quantita, PrezzoUnitario.Obiettivo didattico: consolidare Where, Select, GroupBy, SelectMany e Aggregate in uno scenario realistico.
Scenario: Stai implementando un motore di regole per assegnare sconti a clienti premium.
Consegna:
Func<decimal, decimal> per rappresentare una trasformazione di prezzo.Compose per comporre due funzioni.Obiettivo didattico: applicare pattern funzionali reali con lambda annidate e composizione di funzioni.
Scenario: Hai una coda di job da eseguire in background e vuoi registrare callback diverse per successo, errore e retry.
Consegna:
JobRunner con un metodo Esegui(string nomeJob, Action<string> onSuccess, Action<string> onError).Obiettivo didattico: comprendere callback, closure, lifetime delle variabili catturate e bug classici nei loop.
Prenota una lezione