1. Introduzione
In molti progetti C# moderni ci sono quasi solo handler anonimi invece dei "normali" metodi per gli eventi. È successo perché le espressioni lambda permettono di dichiarare rapidamente e in modo conciso l'handler direttamente nel punto di iscrizione (operatore +=), se la gestione è semplice e non viene riutilizzata in altre parti del codice. È come attaccare un piccolo post-it direttamente sulla macchina del caffè con scritto "premi questo pulsante" invece di scrivere un manuale dettagliato e tenerlo in una cartella separata. Se il task è locale e usa una sola volta, la lambda è perfetta!
Dove si usa nella pratica
- In ASP.NET (per esempio nella gestione degli eventi del ciclo di vita della pagina),
- In WPF/WinForms per l'UI (per esempio al click di un pulsante),
- Nel programming lato server (per esempio logica dentro una pipeline),
- Nei test, quando l'handler non ha bisogno di un nome separato.
Sintassi: come appare nel codice
Cominciamo con un handler "normale":
// Dichiarazione dell'evento
public event EventHandler? MyEvent;
// Iscrizione all'evento con un metodo normale
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("L'evento si è verificato!");
}
public void Subscribe()
{
MyEvent += Handler;
}
Ora — la versione con un'espressione lambda (funzione anonima):
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("L'evento si è verificato (lambda)!");
}
Nota: non creiamo un metodo separato, ma definiamo l'handler direttamente al momento dell'iscrizione. La firma della lambda corrisponde automaticamente al tipo dell'evento (EventHandler).
Confronto degli approcci
| Metodo normale | Lambda | |
|---|---|---|
| Volume di codice | Di più (metodo + iscrizione) | Di meno, tutto sul posto |
| Riutilizzabilità | Può essere riutilizzato | Di solito no |
| Località del codice | Dispersione | Tutto vicino |
| Chiarezza/leggibilità | Buona per logiche complesse | Ottima per casi semplici |
2. Applicazione con gestione degli eventi e lambda
Struttura base
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"Voce di menu {index} selezionata.");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
Iscrizione con un'espressione lambda
class Program
{
static void Main()
{
var menu = new Menu();
// Iscrizione all'evento tramite lambda
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("Grazie per la tua scelta! L'handler lambda è stato chiamato.");
};
menu.SelectItem(1);
}
}
Output atteso:
Voce di menu 1 selezionata.
Grazie per la tua scelta! L'handler lambda è stato chiamato.
Cattura di variabili dal contesto esterno
Uno dei vantaggi delle lambda è che possono "ricordare" valori dal scope esterno (closure). Per esempio, contare quante volte è stata selezionata una voce di menu: la variabile counter viene catturata nella closure.
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"Voce selezionata {counter} volta(e)!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
Output atteso:
Voce di menu 1 selezionata.
Voce selezionata 1 volta(e)!
Voce di menu 2 selezionata.
Voce selezionata 2 volta(e)!
Questa è la magia delle closure in azione: la variabile counter continua a vivere dentro la lambda!
Esempio con parametri dell'evento
Se il tuo evento usa EventHandler<T>, dove T è una classe personalizzata con informazioni aggiuntive, la lambda si "adatta" semplicemente alla firma richiesta.
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"Voce di menu {index}: {description} selezionata.");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// Utilizzo
static void Main()
{
var menu = new Menu();
// Lambda che sfrutta gli argomenti dell'evento
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"Selezionata voce #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "Informazioni sul programma");
}
Output atteso:
Voce di menu 3: Informazioni sul programma selezionata.
Selezionata voce #3: INFORMAZIONI SUL PROGRAMMA
3. Handler locali e lambda
Le lambda sono ideali quando:
- La logica dell'handler è breve e chiara,
- L'handler è usato solo in un punto,
- È necessario "catturare" variabili dal contesto locale.
Se la gestione è complessa, richiede riuso o può essere invocata al di fuori del punto di dichiarazione, è meglio usare un metodo nominato separato.
Esempio: logica dentro vs fuori
Lambda (ideale):
button.Click += (s, e) => MessageBox.Show("Pulsante premuto!");
Metodi (quando la logica è più complessa o serve riuso):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("Dati salvati!");
}
}
4. Sotto il cofano: cosa succede alle lambda-handler
Una lambda è comunque un delegate, come un handler normale. Il compilatore crea "al volo" un metodo anonimo, e se dentro ci sono variabili catturate crea anche una classe nascosta per conservarle.
Importante ricordare (errore comune): se crei una lambda dentro un ciclo e la iscrivi a un evento, tutte le iterazioni potrebbero catturare la stessa variabile di ciclo.
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Click sul pulsante #{i}");
};
}
Per tutti gli handler il numero del pulsante potrebbe risultare 5! Per evitarlo, crea una copia della variabile dentro il ciclo:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // copia locale
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Click sul pulsante #{buttonIndex}");
};
}
Ora tutto funziona come ci si aspetta.
Tutta la potenza degli handler lambda: vita reale
Queste lambda risparmiano tempo e velocizzano lo sviluppo, soprattutto quando la logica è semplice. Rendono il codice "vicino al task", senza spargerlo in file e classi differenti. Nei progetti reali le vedrai ovunque: dalla gestione degli eventi in UI fino alle sottoscrizioni ai messaggi in bus di evento asincroni.
5. Errori tipici
Errore n.1: dimenticare di cancellare l'iscrizione (-=) agli oggetti long-lived.
Se l'oggetto sottoscrittore non si cancella dall'evento dell'editore, il riferimento al delegate mantiene vivo il sottoscrittore in memoria — il garbage collector non può raccoglierlo anche se non ci sono altri riferimenti. Di conseguenza si hanno "memory leak" e dipendenze residue, specialmente quando l'editore vive a lungo (eventi statici, singleton, servizi).
Come evitare: cancellate sempre l'iscrizione durante il rilascio/distruzione (per esempio in Dispose, OnDisable, OnDestroy). Valutate weak references (weak events / WeakEventManager), l'uso del pattern "event manager" o IObservable/Rx se lo scenario è complesso.
Errore n.2: catturare variabili dal ciclo senza copia locale.
Una trappola comune è scrivere la sottoscrizione dentro un ciclo e la closure cattura la stessa variabile di ciclo, quindi tutti gli handler vedono il suo valore finale invece di quello presente quando l'handler è stato creato. Questo porta a risultati inaspettati (tutti gli handler "stampano" lo stesso numero, ecc.).
Come evitare: dentro il ciclo create una copia locale del valore e catturatela:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
Oppure passate il valore necessario a un metodo wrapper. È più affidabile e rende l'intento evidente.
Errore n.3: usare lambda troppo grandi nel punto di iscrizione.
Se metti logica di business voluminosissima direttamente nella funzione anonima alla sottoscrizione, il codice diventa difficile da leggere, testare e debuggare. Inoltre — con lambda anonime è più complesso fare unsubscribe correttamente, perché serve la stessa istanza del delegate. Di conseguenza la logica si disperde e perde struttura.
Come evitare: estrai la logica complessa in metodi nominati o servizi; se necessario conserva il delegate in una variabile/campo in modo da poterlo rimuovere; lascia nella lambda solo un piccolo wrapper/redirect. Questo migliora la leggibilità e la manutenibilità del codice.
GO TO FULL VERSION