CodeGym /Corsi /C# SELF /Gestione degli eventi: espressioni lambda

Gestione degli eventi: espressioni lambda

C# SELF
Livello 52 , Lezione 4
Disponibile

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.

1
Sondaggio/quiz
Eventi in C#, livello 52, lezione 4
Non disponibile
Eventi in C#
Creazione di eventi usando i delegati
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION