Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalUn 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:
System.MulticastDelegate;I delegati servono per:
Quando scrivi:
Action<string> stampa = Console.WriteLine;
non stai salvando “il codice del metodo” dentro una variabile. Stai creando un oggetto delegato che contiene abbastanza informazioni per chiamare in sicurezza un metodo compatibile.
Li incontri continuamente, anche quando non dichiari un delegate personalizzato:
List<T>.ForEach(Action<T>)Enumerable.Where(Func<T, bool>)Task.Run(Action)Comparison<T>Predicate<T>Button.ClickIn altre parole, gran parte del C# moderno usa delegati in modo esplicito o implicito.
Per dichiarare un tipo delegato si utilizza la parola chiave delegate:
[modificatore] delegate [tipo_ritorno] [nome_funzione]([lista_parametri]);
Esempio:
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");
}
}
In questo esempio:
Delegato rappresenta un metodo che non restituisce nulla (void) e accetta una stringa;MetodoStampa è compatibile con la firma del delegato;del("Hello World") equivale concettualmente a MetodoStampa("Hello World").Un metodo può essere assegnato a un delegato se rispetta:
ref, out, in, se presenti.Per esempio:
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;
Operazione op1 = Somma;
Operazione op2 = Moltiplica;
Un delegato può riferirsi sia a:
public delegate void Logger(string messaggio);
public class ConsoleLogger
{
public void Log(string messaggio)
{
Console.WriteLine($"LOG: {messaggio}");
}
}
public class Program
{
public static void LogStatico(string messaggio)
{
Console.WriteLine($"STATICO: {messaggio}");
}
public static void Main()
{
Logger a = LogStatico;
var logger = new ConsoleLogger();
Logger b = logger.Log;
a("ciao");
b("mondo");
}
}
Nel primo caso il target è assente; nel secondo il delegato memorizza anche il riferimento all’oggetto logger.
Ogni delegato dichiarato in C# viene compilato in un tipo simile a una classe. Concettualmente:
System.MulticastDelegate;Invoke(...);BeginInvoke e EndInvoke nei modelli legacy.In forma semplificata, una dichiarazione come:
public delegate int Operazione(int a, int b);
viene trasformata in qualcosa di equivalente a:
public sealed class Operazione : MulticastDelegate
{
public Operazione(object target, IntPtr method);
public int Invoke(int a, int b);
}
Non è codice che scrivi tu manualmente: lo genera il compilatore.
Un’istanza di delegato, a livello concettuale, contiene:
flowchart LR
A[Variabile delegato] --> B[Oggetto delegato]
B --> C[Metodo]
B --> D[Target object opzionale]
B --> E[Invocation list opzionale]
Quando assegni un metodo a un delegato, il compilatore produce istruzioni IL che:
Schema semplificato:
ldnull
ldftn void Program::MetodoStampa(string)
newobj instance void Program/Delegato::.ctor(object, native int)
stloc.0
L’idea chiave è che il delegato non è una “funzione volante”, ma un oggetto costruito a runtime con un riferimento a metodo.
Ha senso dichiarare un tipo delegate dedicato quando:
ValidationRule, RetryPolicy, PaymentProcessor;Func<...>.Un delegato multicast può referenziare più metodi contemporaneamente.
Regole principali:
+ o += per aggiungere metodi;- o -= per rimuoverli;public delegate void Delegato(string messaggio);
public class Program
{
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);
public static void Main()
{
Delegato d = MetodoA;
d += MetodoB;
d += MetodoC;
d("Prova di multicast");
d -= MetodoB;
d("Dopo rimozione di B");
}
}
Un delegato multicast non contiene un solo riferimento, ma una lista di invocazione. Ogni elemento della lista è, concettualmente, una coppia:
Con GetInvocationList() puoi ispezionare questa lista:
public delegate void Notifica(string messaggio);
Notifica notifica = msg => Console.WriteLine("Console: " + msg);
notifica += msg => Console.WriteLine("Audit: " + msg);
Delegate[] handlers = notifica.GetInvocationList();
foreach (Delegate handler in handlers)
{
Console.WriteLine($"Metodo: {handler.Method.Name}");
Console.WriteLine($"Target: {handler.Target}");
}
Questo è molto utile per debugging, logging avanzato o invocazione controllata dei singoli subscriber.
La chaining consiste nel comporre più handler in una sola variabile delegato:
Action pipeline = Step1;
pipeline += Step2;
pipeline += Step3;
pipeline();
È una tecnica comoda per:
Tuttavia non sostituisce automaticamente pattern più strutturati come middleware, command bus o observer complessi.
I delegati sono immutabili: quando fai d += Metodo, in realtà viene creato un nuovo delegato con una nuova invocation list.
Questa caratteristica riduce alcuni problemi di concorrenza, ma non elimina tutti i rischi.
Il problema classico è questo:
if (OnNotifica != null)
{
OnNotifica("messaggio");
}
Tra il controllo != null e l’invocazione, un altro thread potrebbe rimuovere tutti gli handler.
Per questo si preferisce:
OnNotifica?.Invoke("messaggio");
oppure la copia locale:
var copia = OnNotifica;
copia?.Invoke("messaggio");
Con gli eventi, la copia locale resta una tecnica molto usata perché fotografa l’elenco dei subscriber in quel preciso istante.
Se vuoi che tutti gli handler vengano eseguiti anche se uno fallisce, puoi ispezionare la lista e invocare ogni elemento separatamente:
Action azione = HandlerA;
azione += HandlerB;
azione += HandlerC;
foreach (Action handler in azione.GetInvocationList())
{
try
{
handler();
}
catch (Exception ex)
{
Console.WriteLine($"Errore in {handler.Method.Name}: {ex.Message}");
}
}
Quando un delegato rappresenta metodi che ritornano un valore, in una lista multicast solo il valore restituito dall’ultimo metodo viene restituito dalla chiamata diretta al delegato.
Esempio:
public delegate int Calcola();
public class Program
{
public static int Somma() => 10;
public static int Moltiplica() => 5 * 2;
public static void Main()
{
Calcola calc = Somma;
calc += Moltiplica;
int risultato = calc();
Console.WriteLine(risultato);
}
}
Molti principianti pensano che il multicast “raccolga tutti i risultati”.
Non è così: la chiamata calc() invoca tutti i metodi in sequenza, ma ritorna solo l’ultimo valore.
Se vuoi tutti i risultati, devi iterare la invocation list:
public delegate int Calcolo();
Calcolo c = () => 10;
c += () => 20;
c += () => 30;
List<int> risultati = new();
foreach (Calcolo singolo in c.GetInvocationList())
{
risultati.Add(singolo());
}
I delegati con ritorno sono molto utili quando rappresentano:
Esempi reali:
Func<HttpRequest, CancellationToken, Task<HttpResponse>> in pipeline web;Comparison<T> per ordinamenti custom;Converter<TInput, TOutput> per trasformazioni di dati;Un anti-pattern frequente è usare un delegato multicast con ritorno per modellare votazioni, aggregazioni o decisioni multiple. In quei casi è quasi sempre meglio:
C# fornisce delegati generici già pronti all’uso, che evitano di dichiarare nuovi tipi delegate.
Rappresenta un metodo che non restituisce valore (void).
Action<string> stampa = msg => Console.WriteLine(msg);
stampa("Ciao da Action!");
Usi reali comuni:
Caso reale:
public static void EseguiConAudit(Action operazione)
{
Console.WriteLine("Inizio");
operazione();
Console.WriteLine("Fine");
}
Rappresenta un metodo che restituisce un valore.
Func<int, int, int> somma = (a, b) => a + b;
int risultato = somma(5, 4);
L’ultimo tipo generico rappresenta il tipo di ritorno.
Esempio:
Func<string, int> accetta una string e restituisce un int.
Usi reali comuni:
Caso reale:
public static T Misura<T>(Func<T> operazione)
{
var inizio = DateTime.UtcNow;
T risultato = operazione();
var fine = DateTime.UtcNow;
Console.WriteLine($"Durata: {(fine - inizio).TotalMilliseconds} ms");
return risultato;
}
Rappresenta un metodo che restituisce un valore booleano (bool) e accetta un singolo parametro.
Predicate<int> pari = n => n % 2 == 0;
Console.WriteLine(pari(4));
Usi reali comuni:
Find, Exists, RemoveAll);Caso reale:
List<string> email = new() { "a@test.it", "b@azienda.it", "c@test.it" };
Predicate<string> dominioAziendale = e => e.EndsWith("@azienda.it");
bool presente = email.Exists(dominioAziendale);
Usa Action, Func e Predicate quando:
Usa un delegate custom quando:
Esempio:
public delegate decimal CalcoloCommissione(decimal importo, Cliente cliente);
Spesso comunica più di Func<decimal, Cliente, decimal>.
Delegati e interfacce possono entrambi modellare comportamenti, ma servono scopi diversi.
Delegato:
Interfaccia:
Esempio con delegato:
public static void ElaboraOrdine(Ordine ordine, Action<Ordine> callback)
{
callback(ordine);
}
Esempio con interfaccia:
public interface IOrderProcessor
{
void Validate(Ordine ordine);
void Save(Ordine ordine);
void Notify(Ordine ordine);
}
Regola pratica:
È possibile definire un delegato senza dichiarare un metodo separato tramite metodi anonimi.
public delegate void Stampa(string messaggio);
Stampa stampa = delegate(string msg)
{
Console.WriteLine($"Anonimo: {msg}");
};
stampa("Hello!");
Questa forma è utile quando la logica:
Entrambi servono a creare callback inline. La lambda è in genere più compatta e oggi è la forma più usata.
Metodo anonimo:
Action<string> a = delegate(string s)
{
Console.WriteLine(s);
};
Lambda:
Action<string> b = s => Console.WriteLine(s);
Quando una lambda o un metodo anonimo usa variabili del contesto esterno, il compilatore crea una closure. In pratica genera un oggetto ausiliario che contiene quelle variabili come campi.
int contatore = 0;
Action incrementa = () =>
{
contatore++;
Console.WriteLine(contatore);
};
incrementa();
incrementa();
Qui la lambda non copia semplicemente il valore iniziale di contatore.
Cattura la variabile, quindi tutte le invocazioni vedono lo stesso stato condiviso.
Errore tipico:
List<Action> azioni = new();
for (int i = 0; i < 3; i++)
{
azioni.Add(() => Console.WriteLine(i));
}
foreach (var azione in azioni)
{
azione();
}
Molti si aspettano:
0
1
2
ma il risultato può essere:
3
3
3
perché tutte le lambda catturano la stessa variabile i.
La correzione consiste nel creare una variabile locale per iterazione:
List<Action> azioni = new();
for (int i = 0; i < 3; i++)
{
int copia = i;
azioni.Add(() => Console.WriteLine(copia));
}
Le closure possono:
Anti-pattern comuni:
Le lambda expressions sono la forma più comune per creare delegati in C# moderno.
Action<int> stampaQuadrato = n => Console.WriteLine(n * n);
stampaQuadrato(5);
Sono comunemente utilizzate con LINQ e metodi di estensione come Select, Where, OrderBy.
Esempio LINQ:
int[] numeri = { 1, 2, 3, 4, 5 };
var pari = numeri.Where(n => n % 2 == 0);
foreach (var n in pari)
{
Console.WriteLine(n);
}
Func<int, int> doppio = x => x * 2;
Func<int, int> elaborata = x =>
{
int risultato = x * 2;
return risultato + 1;
};
Molto spesso il compilatore deduce automaticamente il tipo del delegato dal contesto:
var numeri = new List<int> { 1, 2, 3, 4, 5 };
var dispari = numeri.Where(n => n % 2 != 0);
La lambda n => n % 2 != 0 viene convertita nel delegate richiesto da Where, cioè Func<int, bool>.
Quasi tutte le lambda non banali usano closure. Per esempio:
int soglia = 10;
Func<int, bool> maggioreDiSoglia = n => n > soglia;
Se soglia cambia, cambia anche il comportamento della lambda:
soglia = 100;
Console.WriteLine(maggioreDiSoglia(50));
Le lambda vengono usate per:
Esempio più realistico:
public static IEnumerable<Ordine> FiltraOrdini(
IEnumerable<Ordine> ordini,
Func<Ordine, bool> filtro)
{
return ordini.Where(filtro);
}
Best practice:
Gli eventi in C# si basano sui delegati. Un evento è un meccanismo che permette a una classe di notificare a una o più altre classi che qualcosa è successo, ma con una differenza fondamentale: dall’esterno puoi iscriverti o disiscriverti, non puoi invocare direttamente l’evento.
Esempio:
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}");
}
}
public class Program
{
public static void Main()
{
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
pub.OnNotifica += sub.Ricevi;
pub.Invia();
}
}
Se dichiari:
public event Notifica OnNotifica;
il compilatore genera un meccanismo che espone solo:
addremovema non l’assegnazione completa dall’esterno.
Quindi il codice client può fare:
publisher.OnNotifica += handler;
publisher.OnNotifica -= handler;
ma non può fare:
publisher.OnNotifica("ciao");
né sostituire arbitrariamente la lista di subscriber.
Puoi definire esplicitamente gli accessors dell’evento:
public class Publisher
{
private EventHandler? _notifica;
public event EventHandler Notifica
{
add
{
Console.WriteLine("Subscriber aggiunto");
_notifica += value;
}
remove
{
Console.WriteLine("Subscriber rimosso");
_notifica -= value;
}
}
public void Invia()
{
_notifica?.Invoke(this, EventArgs.Empty);
}
}
Questo torna utile quando vuoi:
EventHandler ed EventHandler<TEventArgs>Il pattern idiomatico .NET per gli eventi è:
object? senderEventArgs eoppure una sottoclasse di EventArgs.
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));
}
}
Vantaggi:
EventArgs.Il pattern classico consiste nel:
EventArgs;EventHandler<TEventArgs>;this come sender.sequenceDiagram
participant P as Publisher
participant E as Evento
participant S as Subscriber
S->>E: subscribe
P->>E: Invoke(this, eventArgs)
E->>S: handler(sender, e)
Gli eventi ereditano i problemi di concorrenza dei delegati multicast. La forma consigliata di invocazione resta:
var handler = DownloadCompleted;
handler?.Invoke(this, new DownloadCompletedEventArgs("report.pdf", 1200));
Perché:
Action al posto di EventHandler<TEventArgs> in API pubbliche, perdendo chiarezza semantica;EventHandler o EventHandler<TEventArgs>;EventArgs.Empty quando non devi trasportare dati;this come sender;I delegati supportano covarianza e controvarianza, cioè la possibilità di usare tipi derivati o base in modo compatibile con la firma del delegato.
public class Animale { }
public class Cane : Animale { }
public delegate Animale CreaAnimale();
public delegate void GestisciAnimale(Cane c);
CreaAnimale d1 = () => new Cane();
GestisciAnimale d2 = (Animale a) => Console.WriteLine(a.GetType().Name);
Permette di riusare callback in gerarchie di tipi senza dover creare adattatori inutili.
Esempi reali:
Animale può restituire un Cane;Animale può essere riusato dove serve un handler per Cane;Nel design reale, il dubbio più frequente non è “covarianza o controvarianza”, ma:
Usa un delegato quando:
Usa un’interfaccia quando:
Errori tipici con i delegati:
Action, Func e Predicate per casi semplici e tecnici;EventHandler<TEventArgs>;GetInvocationList();Cos'è un delegato in C#?
Quale keyword si usa per dichiarare un tipo delegato?
Cosa fa l'operatore += su un delegato?
In un delegato multicast con tipo di ritorno, quale valore viene mantenuto?
Quale delegato predefinito rappresenta un metodo che non restituisce valore?
Func<string, int> rappresenta:
Qual è la differenza tra un metodo anonimo e una lambda expression?
Gli eventi in C# si basano su:
La covarianza in un delegato permette di:
Quale operatore si usa per rimuovere un metodo da un delegato multicast?
Scenario: Vuoi creare un piccolo motore di calcolo personalizzabile, dove l’utente può scegliere che tipo di operazione applicare su due numeri.
Consegna:
Operazione(int a, int b) che restituisce un int.Somma(int a, int b)Differenza(int a, int b)Moltiplicazione(int a, int b)EseguiOperazione(int a, int b, Operazione op) che invoca il delegato passato.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.
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:
Logger(string messaggio).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)).RegistraOperazione(string azione, Logger log) che invoca il logger passato.Main, esegui varie chiamate con diversi logger e mostra come cambia il comportamento.Obiettivo didattico: imparare a decouplare la logica di logging dal codice principale, passando comportamenti dinamici tramite delegati.
Scenario:
Hai una lista di numeri interi e vuoi filtrarla in modo dinamico (pari, dispari, maggiori di N, ecc.) usando delegati o Predicate<int>.
Consegna:
Filtra(List<int> numeri, Predicate<int> condizione) che restituisce una nuova lista contenente solo gli elementi che rispettano la condizione.Obiettivo didattico: comprendere come usare delegati predefiniti (Predicate, Func) per scrivere codice generico e riutilizzabile.
Scenario: Vuoi simulare un contatore di download che notifica gli utenti quando viene raggiunto un certo numero di download.
Consegna:
DownloadHandler(string messaggio).DownloadManager con:
OnLimiteRaggiuntoconteggioRegistraDownload() che incrementa il contatore e, se arriva a 5, genera l’evento.EmailNotifier che stampa: “Email inviata: messaggio”ConsoleNotifier che stampa: “Notifica: messaggio”Main, collega entrambe le classi all’evento e simula più download.Obiettivo didattico: collegare il concetto di delegato → evento → subscriber, come accade nei sistemi reali.
Prenota una lezione