Delegates
- #csharp
- #programmazione
- #delegati
In questa lezione
- 1. Introduzione
- 2. Dichiarazione
- 2.1 Passare un comportamento come parametro
- 2.2 Delegati statici e di istanza
- 3. Multicasting di un delegato
- 3.1 Invocation list ed eccezioni
- 3.2 Immutabilità e thread safety
- 4. Delegati con tipo di ritorno
- 5. Delegati predefiniti: Action, Func e Predicate
- 5.1 Esempi d’uso reali
- 5.2 Delegato o interfaccia?
- 6. Metodi anonimi e lambda expressions
- 6.1 Closure
- 6.2 Il capture trap nei cicli
- 7. Delegati ed eventi
- 7.1 Pattern standard: EventHandler<TEventArgs>
- 8. Covarianza e controvarianza
- 9. Quiz
- 10. Esercizi
- 10.1 Esercizio
- 10.2 Esercizio
- 10.3 Esercizio
- 10.4 Esercizio
1. Introduzione
Un delegato è un tipo reference di .NET che incapsula in modo sicuro un riferimento a uno o più metodi compatibili con una certa firma. Spesso viene descritto come un “puntatore a funzione tipizzato”, ma in C# è qualcosa di più strutturato:
- è un tipo vero e proprio, generato come classe derivata da
System.MulticastDelegate; - conserva il metodo da invocare e, per i metodi di istanza, anche il target object;
- può contenere una lista di invocazione (più metodi);
- il compilatore controlla parametri, tipo di ritorno e varianza.
I delegati servono per passare comportamenti come parametri, implementare callback e strategie, costruire eventi e alimentare lambda e LINQ.
Li usi continuamente anche senza accorgertene: List<T>.ForEach(Action<T>), Enumerable.Where(Func<T, bool>), Task.Run(Action), Button.Click.
Action<string> stampa = Console.WriteLine;
stampa("Ciao"); // Ciao
Qui non stai salvando “il codice del metodo” in una variabile: stai creando un oggetto delegato che sa come chiamare in sicurezza Console.WriteLine.
2. Dichiarazione
Per dichiarare un tipo delegato si usa la parola chiave delegate:
[modificatore] delegate [tipo_ritorno] [nome_delegato]([lista_parametri]);
public class Program
{
public delegate void Delegato(string messaggio);
public static void MetodoStampa(string messaggio)
{
Console.WriteLine(messaggio);
}
public static void Main()
{
Delegato del = MetodoStampa;
del("Hello World"); // equivale a MetodoStampa("Hello World")
}
}
Un metodo è assegnabile a un delegato se ha lo stesso numero di parametri, tipi compatibili, tipo di ritorno compatibile e lo stesso uso di ref, out, in.
2.1 Passare un comportamento come parametro
Il vero vantaggio emerge quando il delegato diventa un parametro: lo stesso metodo cambia comportamento a seconda della funzione ricevuta.
public delegate int Operazione(int a, int b);
public static int Somma(int x, int y) => x + y;
public static int Moltiplica(int x, int y) => x * y;
public static int Applica(int a, int b, Operazione op) => op(a, b);
Console.WriteLine(Applica(3, 4, Somma)); // 7
Console.WriteLine(Applica(3, 4, Moltiplica)); // 12
2.2 Delegati statici e di istanza
Un delegato può riferirsi sia a un metodo statico sia a un metodo di istanza. Nel secondo caso memorizza anche l’oggetto su cui chiamarlo.
public delegate void Logger(string messaggio);
public class ConsoleLogger
{
public string Prefisso { get; set; } = "LOG";
public void Log(string messaggio) => Console.WriteLine($"{Prefisso}: {messaggio}");
}
public static void LogStatico(string messaggio) => Console.WriteLine($"STATICO: {messaggio}");
Logger a = LogStatico;
var logger = new ConsoleLogger();
Logger b = logger.Log;
a("ciao"); // STATICO: ciao
logger.Prefisso = "DBG";
b("mondo"); // DBG: mondo (usa lo stato attuale dell'oggetto)
Console.WriteLine(a.Target == null); // True
Nota
Ogni delegate viene compilato in una classe sealed che eredita da MulticastDelegate, con un costruttore e un metodo Invoke(...). Scrivere del("x") equivale a del.Invoke("x"): il delegato è un oggetto creato a runtime, non una “funzione volante”.
Ha senso dichiarare un delegate personalizzato quando il nome comunica un’intenzione di dominio (ValidationRule, CalcoloCommissione) meglio di un generico Func<...>; per il resto si usano i delegati predefiniti (sezione 5).
3. Multicasting di un delegato
Un delegato multicast referenzia più metodi:
+o+=aggiungono un metodo,-o-=lo rimuovono;- i metodi vengono invocati in ordine di registrazione.
public delegate void Delegato(string messaggio);
public static void MetodoA(string msg) => Console.WriteLine("A: " + msg);
public static void MetodoB(string msg) => Console.WriteLine("B: " + msg);
public static void MetodoC(string msg) => Console.WriteLine("C: " + msg);
Delegato d = MetodoA;
d += MetodoB;
d += MetodoC;
d("prova"); // A: prova / B: prova / C: prova
d -= MetodoB;
d("dopo"); // A: dopo / C: dopo
Attenzione
Rimuovere una lambda con -= non funziona se scrivi di nuovo la stessa lambda: d -= msg => Console.WriteLine(msg); crea un nuovo delegato e non rimuove nulla. Se devi disiscriverti, salva la lambda in una variabile.
3.1 Invocation list ed eccezioni
Con GetInvocationList() ottieni i singoli delegati della catena. È utile soprattutto per un motivo: se un handler lancia un’eccezione, quelli successivi non vengono eseguiti. Invocandoli uno per uno puoi isolare gli errori:
Action azione = () => Console.WriteLine("Handler A");
azione += () => throw new InvalidOperationException("boom");
azione += () => Console.WriteLine("Handler C");
foreach (Action handler in azione.GetInvocationList())
{
try
{
handler();
}
catch (Exception ex)
{
Console.WriteLine($"Errore: {ex.Message}");
}
}
// Handler A
// Errore: boom
// Handler C
3.2 Immutabilità e thread safety
I delegati sono immutabili: d += Metodo crea un nuovo delegato con una nuova lista. Resta però un rischio classico:
if (OnNotifica != null)
{
OnNotifica("messaggio"); // un altro thread potrebbe averlo appena reso null
}
La forma corretta è leggere il delegato una sola volta:
OnNotifica?.Invoke("messaggio");
// equivalente esplicito: copia locale
var copia = OnNotifica;
copia?.Invoke("messaggio");
4. Delegati con tipo di ritorno
In un delegato multicast con valore di ritorno vengono eseguiti tutti i metodi, ma la chiamata restituisce solo il valore dell’ultimo.
public delegate int Calcola();
Calcola calc = () => 10;
calc += () => 20;
calc += () => 30;
Console.WriteLine(calc()); // 30
Se ti servono tutti i risultati devi iterare la invocation list:
List<int> risultati = new();
foreach (Calcola singolo in calc.GetInvocationList())
{
risultati.Add(singolo());
}
Console.WriteLine(string.Join(", ", risultati)); // 10, 20, 30
Attenzione
Non usare un delegato multicast con ritorno per modellare votazioni o aggregazioni: è un errore frequente. Meglio una List<Func<T>> esplicita su cui applicare Select, Sum, All o simili.
I delegati con ritorno sono invece perfetti come singola strategia: calcolo di uno sconto, mapping, factory, comparatore (Comparison<T>).
5. Delegati predefiniti: Action, Func e Predicate
C# fornisce delegati generici già pronti, che nella maggior parte dei casi evitano di dichiarare nuovi tipi delegate.
| Delegato | Ritorno | Esempio |
|---|---|---|
Action<T1, ...> | void | Action<string> log |
Func<T1, ..., TResult> | TResult (ultimo parametro generico) | Func<string, int> lunghezza |
Predicate<T> | bool, un solo parametro | Predicate<int> pari |
Action<string> stampa = msg => Console.WriteLine(msg);
Func<int, int, int> somma = (a, b) => a + b;
Func<string, int> lunghezza = s => s.Length;
Predicate<int> pari = n => n % 2 == 0;
stampa("Ciao"); // Ciao
Console.WriteLine(somma(5, 4)); // 9
Console.WriteLine(lunghezza("C#")); // 2
Console.WriteLine(pari(4)); // True
5.1 Esempi d’uso reali
Action per avvolgere un’operazione con logica comune, Func<T> per misurarne il tempo restituendo il risultato:
public static void EseguiConAudit(Action operazione)
{
Console.WriteLine("Inizio");
operazione();
Console.WriteLine("Fine");
}
public static T Misura<T>(Func<T> operazione)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
T risultato = operazione();
Console.WriteLine($"Durata: {sw.ElapsedMilliseconds} ms");
return risultato;
}
EseguiConAudit(() => Console.WriteLine("Salvataggio..."));
long totale = Misura(() => Enumerable.Range(1, 1_000_000).Sum(x => (long)x));
Predicate<T> è usato dai metodi di List<T> come Find, Exists, RemoveAll:
List<string> email = new() { "a@test.it", "b@azienda.it", "c@test.it" };
Predicate<string> aziendale = e => e.EndsWith("@azienda.it");
Console.WriteLine(email.Exists(aziendale)); // True
email.RemoveAll(aziendale);
Console.WriteLine(email.Count); // 2
Suggerimento
Regola pratica: Action/Func per codice tecnico e firme semplici; un delegate custom quando il nome aggiunge significato. delegate decimal CalcoloCommissione(decimal importo, Cliente c) dice molto più di Func<decimal, Cliente, decimal>.
5.2 Delegato o interfaccia?
Entrambi modellano comportamenti, ma con scopi diversi:
- delegato: “passami una funzione”. Ideale per una singola operazione, callback brevi, strategie semplici; si integra con lambda e LINQ;
- interfaccia: “dammi un collaboratore”. Meglio quando il contratto ha più metodi correlati, stato interno o dipendenze.
// Delegato: basta una funzione
public static void ElaboraOrdine(Ordine ordine, Action<Ordine> callback) => callback(ordine);
// Interfaccia: più responsabilità correlate
public interface IOrderProcessor
{
void Validate(Ordine ordine);
void Save(Ordine ordine);
void Notify(Ordine ordine);
}
6. Metodi anonimi e lambda expressions
Puoi creare un delegato inline, senza dichiarare un metodo separato. La forma storica è il metodo anonimo, quella moderna (e più usata) è la lambda con =>:
// Metodo anonimo
Action<string> a = delegate(string s) { Console.WriteLine($"Anonimo: {s}"); };
// Lambda con espressione singola
Action<string> b = s => Console.WriteLine($"Lambda: {s}");
// Lambda con body a blocco
Func<int, int> elaborata = x =>
{
int doppio = x * 2;
return doppio + 1;
};
a("ciao"); // Anonimo: ciao
b("ciao"); // Lambda: ciao
Console.WriteLine(elaborata(5)); // 11
Molto spesso il compilatore deduce il tipo di delegato dal contesto: in numeri.Where(n => n % 2 != 0) la lambda diventa un Func<int, bool>.
var numeri = new List<int> { 5, 2, 8, 1, 4 };
var risultato = numeri
.Where(n => n > 1) // Func<int, bool>
.OrderBy(n => n) // Func<int, int>
.Select(n => $"#{n}"); // Func<int, string>
Console.WriteLine(string.Join(" ", risultato)); // #2 #4 #5 #8
Suggerimento
Se una lambda supera poche righe, estraila in un metodo con un nome chiaro: puoi passarlo direttamente al posto della lambda (numeri.Where(IsValido)) e il codice diventa più leggibile e testabile.
6.1 Closure
Quando una lambda usa variabili esterne, il compilatore crea una closure: la lambda cattura la variabile, non una copia del suo valore.
int contatore = 0;
Action incrementa = () => contatore++;
incrementa();
incrementa();
Console.WriteLine(contatore); // 2
int soglia = 10;
Func<int, bool> maggiore = n => n > soglia;
Console.WriteLine(maggiore(50)); // True
soglia = 100;
Console.WriteLine(maggiore(50)); // False: la lambda vede il nuovo valore
Le closure sono comode per creare funzioni “configurate”, come una factory di moltiplicatori:
static Func<int, int> CreaMoltiplicatore(int fattore) => x => x * fattore;
var doppio = CreaMoltiplicatore(2);
var triplo = CreaMoltiplicatore(3);
Console.WriteLine(doppio(5)); // 10
Console.WriteLine(triplo(5)); // 15
6.2 Il capture trap nei cicli
List<Action> azioni = new();
for (int i = 0; i < 3; i++)
{
azioni.Add(() => Console.WriteLine(i));
}
foreach (var azione in azioni) azione(); // 3 3 3
Tutte le lambda catturano la stessa variabile i, che a fine ciclo vale 3. La correzione è una copia locale per ogni iterazione:
for (int i = 0; i < 3; i++)
{
int copia = i;
azioni.Add(() => Console.WriteLine(copia)); // 0 1 2
}
Pericolo
Le closure mantengono vivi gli oggetti catturati finché vive il delegato. Catturare variabili mutabili in callback asincrone o su più thread crea stato condiviso invisibile e bug difficili da riprodurre: cattura solo ciò che serve, preferibilmente valori immutabili.
7. Delegati ed eventi
Gli eventi sono costruiti sui delegati, con una restrizione fondamentale: dall’esterno della classe puoi solo iscriverti (+=) o disiscriverti (-=), non puoi invocare l’evento né sostituire la lista dei subscriber.
public delegate void Notifica(string message);
public class Publisher
{
public event Notifica? OnNotifica;
public void Invia() => OnNotifica?.Invoke("Evento generato!");
}
public class Subscriber
{
public void Ricevi(string msg) => Console.WriteLine($"Ricevuto: {msg}");
}
var pub = new Publisher();
var sub = new Subscriber();
pub.OnNotifica += sub.Ricevi;
pub.Invia(); // Ricevuto: Evento generato!
// pub.OnNotifica("ciao"); // errore di compilazione: fuori dalla classe
// pub.OnNotifica = null; // errore di compilazione
Senza la keyword event, un campo delegato pubblico potrebbe essere sovrascritto con = da chiunque, cancellando tutti gli altri subscriber.
Nota
Il compilatore genera per ogni evento due accessors, add e remove. Puoi scriverli a mano (event EventHandler X { add { ... } remove { ... } }) per loggare le iscrizioni o gestire la sincronizzazione, ma è raramente necessario.
7.1 Pattern standard: EventHandler<TEventArgs>
Il pattern idiomatico .NET prevede handler con firma (object? sender, TEventArgs e):
- crea una classe dati che eredita da
EventArgs; - dichiara l’evento come
EventHandler<TEventArgs>; - invocalo passando
thiscomesender.
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public class Downloader
{
public event EventHandler<DownloadCompletedEventArgs>? DownloadCompleted;
public void Complete(string fileName, long size) =>
DownloadCompleted?.Invoke(this, new DownloadCompletedEventArgs(fileName, size));
}
var downloader = new Downloader();
downloader.DownloadCompleted += (sender, e) =>
Console.WriteLine($"Scaricato {e.FileName} ({e.Size} byte)");
downloader.Complete("report.pdf", 1200); // Scaricato report.pdf (1200 byte)
Se non devi trasportare dati usa EventHandler e passa EventArgs.Empty.
Pericolo
Il publisher mantiene un riferimento a ogni subscriber iscritto. Se un oggetto di breve durata (una finestra, un view model) si iscrive a un publisher di lunga durata e non fa mai -=, non verrà mai raccolto dal garbage collector: è una delle cause più comuni di memory leak in .NET.
8. Covarianza e controvarianza
I delegati permettono di usare tipi più specifici o più generici in modo sicuro rispetto alla firma:
- covarianza (ritorno): un delegato che promette un
Animalepuò restituire unCane; - controvarianza (parametri): un metodo che accetta un
Animalepuò essere usato dove serve un handler perCane.
public class Animale { }
public class Cane : Animale { }
public delegate Animale CreaAnimale();
public delegate void GestisciCane(Cane c);
static Cane NuovoCane() => new Cane();
static void Descrivi(Animale a) => Console.WriteLine(a.GetType().Name);
CreaAnimale crea = NuovoCane; // covarianza: ritorna un tipo derivato
GestisciCane gestisci = Descrivi; // controvarianza: accetta un tipo base
gestisci(new Cane()); // Cane
Lo stesso vale per i delegati generici, grazie ai modificatori out e in nelle loro dichiarazioni (Func<out TResult>, Action<in T>):
Func<Cane> creaCane = () => new Cane();
Func<Animale> creaAnimale = creaCane; // OK: covarianza
Action<Animale> logAnimale = a => Console.WriteLine("Animale");
Action<Cane> logCane = logAnimale; // OK: controvarianza
Questa flessibilità evita di scrivere adattatori inutili quando si lavora con gerarchie di tipi.
9. Quiz
Mettiti alla prova
0/10 risposte
Cos'è un delegato in C#?
Cosa fa l'operatore += su un delegato?
In un delegato multicast, cosa succede se il secondo di tre handler lancia un'eccezione?
In un delegato multicast con tipo di ritorno, quale valore viene restituito?
Quale delegato predefinito rappresenta un metodo che non restituisce valore?
Func<string, int> rappresenta:
Cosa stampa un ciclo for che aggiunge () => Console.WriteLine(i) a una lista per i da 0 a 2, se poi si invocano le lambda?
Quale operazione NON è consentita su un evento dall'esterno della classe che lo dichiara?
La covarianza in un delegato permette di:
Qual è la forma consigliata per invocare un delegato che potrebbe essere null?
10. Esercizi
10.1 Esercizio
Scenario: Vuoi creare un piccolo motore di calcolo personalizzabile, dove l’utente può scegliere che tipo di operazione applicare su due numeri.
Consegna:
- Crea un delegato
Operazione(int a, int b)che restituisce un int. - Implementa tre metodi statici:
Somma(int a, int b)Differenza(int a, int b)Moltiplicazione(int a, int b)
- Scrivi un metodo
EseguiOperazione(int a, int b, Operazione op)che invoca il delegato passato. - Nel
Main, chiedi all’utente di scegliere l’operazione da eseguire e usa il delegato corrispondente.
Obiettivo didattico: capire come passare comportamenti come parametri, e come cambiare la logica di un metodo a runtime.
10.2 Esercizio
Scenario: Hai un’applicazione che deve registrare log in modo diverso a seconda del contesto (console, file, o rete). Vuoi che il tipo di logging sia pluggabile tramite delegati.
Consegna:
- Crea un delegato
Logger(string messaggio). - Implementa tre metodi diversi:
LogConsole(string msg)→ scrive in console.LogFile(string msg)→ scrive su file log.txt.LogNetwork(string msg)→ simula l’invio di un log in rete (es.Console.WriteLine("Inviato: " + msg)).
- Crea un metodo
RegistraOperazione(string azione, Logger log)che invoca il logger passato. - Nel
Main, esegui varie chiamate con diversi logger e poi combina console e file in un unico delegato multicast.
Obiettivo didattico: imparare a disaccoppiare la logica di logging dal codice principale, passando comportamenti dinamici tramite delegati.
10.3 Esercizio
Scenario:
Hai una lista di numeri interi e vuoi filtrarla in modo dinamico (pari, dispari, maggiori di N, ecc.) usando Predicate<int>.
Consegna:
- Crea una lista di interi da 1 a 20.
- Crea un metodo
Filtra(List<int> numeri, Predicate<int> condizione)che restituisce una nuova lista contenente solo gli elementi che rispettano la condizione. - Usa il metodo tre volte con filtri diversi:
- Numeri pari
- Maggiori di N (con N letto da una variabile catturata dalla lambda)
- Multipli di 3
- Stampa i risultati a video.
Obiettivo didattico: comprendere come usare delegati predefiniti e closure per scrivere codice generico e riutilizzabile.
10.4 Esercizio
Scenario: Vuoi simulare un contatore di download che notifica gli utenti quando viene raggiunto un certo numero di download.
Consegna:
- Crea una classe
LimiteRaggiuntoEventArgs : EventArgscon la proprietàConteggio. - Crea una classe
DownloadManagercon:- Un evento
EventHandler<LimiteRaggiuntoEventArgs>? LimiteRaggiunto - Un campo
conteggio - Un metodo
RegistraDownload()che incrementa il contatore e, se arriva a 5, genera l’evento.
- Un evento
- Crea due classi “ascoltatrici”:
EmailNotifierche stampa: “Email inviata: limite di N download”ConsoleNotifierche stampa: “Notifica: limite di N download”
- Nel
Main, collega entrambe le classi all’evento, simula più download e infine disiscriviEmailNotifier.
Obiettivo didattico: collegare il concetto di delegato → evento → subscriber, come accade nei sistemi reali.