Delegati, espressioni lambda ed eventi in C#

Delegati, espressioni lambda ed eventi in C#

Finora abbiamo trattato i metodi come blocchi di codice da richiamare. C# offre però la possibilità di trattare i metodi stessi come valori, da assegnare a variabili, passare come argomenti e memorizzare in collezioni. Questa capacità apre la strada a uno stile di programmazione più flessibile e sta alla base di molte funzionalità del linguaggio, da LINQ alla gestione degli eventi. In questo articolo esploriamo delegati, espressioni lambda ed eventi.

I delegati

Un delegato è un tipo che rappresenta un riferimento a un metodo con una firma specifica. Si può pensare a un delegato come a un "puntatore a funzione" fortemente tipizzato: definisce quali parametri accetta e quale valore restituisce, e può fare riferimento a qualsiasi metodo compatibile con quella firma.

// Dichiarazione di un tipo delegato
delegate int Operation(int a, int b);

class Program
{
    static int Add(int a, int b) => a + b;
    static int Multiply(int a, int b) => a * b;

    static void Main()
    {
        // Il delegato fa riferimento a un metodo compatibile
        Operation op = Add;
        Console.WriteLine(op(3, 4)); // Stampa 7

        // Puo essere riassegnato a un altro metodo
        op = Multiply;
        Console.WriteLine(op(3, 4)); // Stampa 12
    }
}

I delegati predefiniti: Func, Action e Predicate

Nella pratica si dichiarano raramente delegati personalizzati, perché la libreria standard fornisce delegati generici pronti all'uso. Func rappresenta un metodo che restituisce un valore, Action uno che non restituisce nulla e Predicate uno che restituisce un booleano.

// Func: gli ultimi parametri di tipo sono gli argomenti, l'ultimo e il ritorno
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // Stampa 5

// Action: non restituisce nulla
Action<string> log = message => Console.WriteLine($"LOG: {message}");
log("Avvio completato");

// Predicate: restituisce un booleano
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // Stampa true

Le espressioni lambda

Negli esempi precedenti abbiamo già usato le espressioni lambda: sono funzioni anonime, cioè metodi senza nome definiti direttamente nel punto in cui servono. La sintassi usa il simbolo => per separare i parametri dal corpo. Se il corpo è una singola espressione, il valore viene restituito implicitamente; se richiede più istruzioni, si usano le parentesi graffe.

// Lambda a espressione singola
Func<int, int> square = x => x * x;

// Lambda con corpo a blocco
Func<int, string> classify = n =>
{
    if (n > 0) return "positivo";
    if (n < 0) return "negativo";
    return "zero";
};

Un aspetto potente delle lambda è la capacità di catturare le variabili dell'ambiente circostante, creando quella che si chiama una closure. La funzione anonima ricorda il contesto in cui è stata creata anche quando viene eseguita altrove.

int factor = 10;

// La lambda cattura la variabile factor
Func<int, int> multiply = x => x * factor;

Console.WriteLine(multiply(5)); // Stampa 50

Passare metodi come argomenti

La vera utilità dei delegati emerge quando si passa un comportamento a un metodo. Molte funzioni della libreria standard accettano un delegato per personalizzare la propria logica. Ad esempio, l'ordinamento di una lista può ricevere una lambda che stabilisce il criterio di confronto.

var names = new List<string> { "Marco", "Anna", "Giuseppe", "Eva" };

// Ordina in base alla lunghezza, passando il criterio come lambda
names.Sort((first, second) => first.Length.CompareTo(second.Length));

// Un metodo che accetta un comportamento come parametro
static void Repeat(int times, Action<int> action)
{
    for (int i = 0; i < times; i++)
    {
        action(i);
    }
}

Repeat(3, index => Console.WriteLine($"Iterazione {index}"));

Gli eventi

Gli eventi sono un meccanismo costruito sopra i delegati per implementare il pattern di notifica publisher-subscriber. Una classe che pubblica un evento permette ad altre di sottoscriversi per essere avvisate quando accade qualcosa, senza che il pubblicatore conosca in anticipo i destinatari. La parola chiave event protegge il delegato consentendo agli abbonati solo di aggiungersi o rimuoversi.

class Button
{
    // Dichiarazione dell'evento basato sul delegato EventHandler
    public event EventHandler? Clicked;

    public void Press()
    {
        Console.WriteLine("Pulsante premuto");
        // Notifica gli abbonati, se presenti
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main()
    {
        var button = new Button();

        // Sottoscrizione all'evento con +=
        button.Clicked += (sender, args) => Console.WriteLine("Reazione al click");

        button.Press();
    }
}

L'operatore += aggiunge un gestore all'evento, mentre -= lo rimuove. Un evento può avere più abbonati, che vengono tutti notificati in sequenza. L'operatore di accesso condizionale ?. prima di Invoke garantisce che la notifica avvenga solo se esiste almeno un abbonato, evitando un'eccezione.

Il modello standard degli eventi

Per convenzione, gli eventi in .NET seguono uno schema preciso: il gestore riceve due parametri, il mittente dell'evento e un oggetto che eredita da EventArgs contenente i dati aggiuntivi. Definendo una propria classe di argomenti si possono trasmettere informazioni specifiche al momento della notifica.

// Dati personalizzati trasmessi dall'evento
class TemperatureChangedEventArgs : EventArgs
{
    public double NewTemperature { get; }

    public TemperatureChangedEventArgs(double temperature)
    {
        NewTemperature = temperature;
    }
}

class Thermostat
{
    public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;

    private double _temperature;

    public double Temperature
    {
        get => _temperature;
        set
        {
            _temperature = value;
            TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(value));
        }
    }
}

Conclusione

Abbiamo imparato a trattare i metodi come valori tramite i delegati, sfruttando quelli predefiniti Func, Action e Predicate, e a scrivere funzioni anonime con le espressioni lambda, incluse le closure che catturano il contesto. Abbiamo visto come passare comportamenti ai metodi e come gli eventi implementino il pattern di notifica tra oggetti secondo il modello standard di .NET. Questi strumenti sono il fondamento su cui poggia LINQ, l'argomento del prossimo articolo.