1. Callback e programmazione asincrona
Callback (callback) — è il meccanismo per passare un metodo che deve essere chiamato al termine di un'operazione. Molto usato in operazioni asincrone, timer, elaborazione dati e interfacce utente.
Esempio 1: Operazione asincrona con callback
Immagina un'app dove l'utente inserisce una richiesta e il risultato arriva con ritardo (per esempio da internet). Dopo aver ricevuto i dati vogliamo aggiornare lo schermo.
// Delegato per il callback
public delegate void DataReceivedHandler(string result);
// Meccanismo di download asincrono (simulazione)
public void DownloadDataAsync(DataReceivedHandler callback)
{
// Supponiamo che il download richieda tempo (simuliamo con un timer)
Task.Delay(1000).ContinueWith(_ =>
{
string data = "Risultati ricerca: <dati>";
callback(data); // Chiamata del delegato-callback
});
}
// Uso:
DownloadDataAsync(result =>
{
Console.WriteLine("Ricevuto: " + result);
});
Questo approccio permette di scrivere codice molto flessibile, dove la logica dopo aver ricevuto il risultato è completamente separata dalla meccanica di ottenere i dati.
2. Delegati come parametri dei metodi: strategy e comparators
Compito comune: permettere all'utente di passare la "logica" (una funzione) al tuo metodo, in modo che possa decidere come confrontare, filtrare o trasformare gli elementi.
Esempio 2: Implementazione del pattern Strategy tramite delegati
Supponiamo di avere un algoritmo di ordinamento, ma vogliamo che l'utente possa ordinare in modi diversi — per nome, per data, per dimensione ecc.
public delegate bool CompareFunc(int a, int b);
public void BubbleSort(int[] arr, CompareFunc compare)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length - 1; j++)
{
if (compare(arr[j], arr[j + 1]))
{
// Scambiamo
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Ordinamento da maggiore a minore
CompareFunc descending = (a, b) => a < b;
// Uso
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Mostra: 4, 3, 2, 1
Questo pattern è un modo universale per iniettare la propria "strategia" in codice esterno senza modificarne i sorgenti.
3. Metodi anonimi, lambda-expression e delegati
Con l'evoluzione di C# è scomodo dichiarare una classe o un metodo separato per ogni piccolo task. Per fortuna esistono metodi anonimi e lambda-expression, che permettono di creare delegati "al volo".
Esempio 3: Lambda come delegato
Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15
Esempio 4: Scelta dell'operazione per nome (switch + Delegates)
Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"
switch (userInput)
{
case "sum": op = (a, b) => a + b; break;
case "sub": op = (a, b) => a - b; break;
case "mul": op = (a, b) => a * b; break;
case "div": op = (a, b) => a / b; break;
default: throw new Exception("Operazione sconosciuta!");
}
Console.WriteLine(op(6, 2));
Non dimenticare la convalida degli input — i delegati qui danno flessibilità e leggibilità.
4. Delegati e catene di elaborazione (“chain of responsibility”)
Dato che i delegati supportano la multicast, permettono di costruire facilmente catene di handler.
Esempio 5: Catena di filtri
Immaginiamo di avere dei "filtri" che devono processare una stringa.
public delegate string StringFilter(string input);
string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();
StringFilter filters = RemoveDigits;
filters += ToUpper;
// Il delegato passerà la stringa attraverso tutti i filtri
string text = "Ciao123";
foreach (StringFilter filter in filters.GetInvocationList())
{
text = filter(text);
}
Console.WriteLine(text); // Mostra: "CIAO"
Importante: se chiami semplicemente filters(text), verrà restituito il valore solo dell'ultimo handler, non di tutta la catena! Se vuoi il "passaggio" del valore, itera esplicitamente con GetInvocationList(), come sopra.
5. Delegati per il binding dinamico del comportamento a runtime
Prima, per sostituire un comportamento bisognava creare classi e interfacce. Con delegati e lambda puoi esprimere gran parte del "piccolo" polimorfismo come funzioni.
Esempio 6: Comportamento di un robot con comando dinamico
public class Robot
{
public event Action<string>? OnCommandReceived;
public void ReceiveCommand(string command)
{
OnCommandReceived?.Invoke(command);
}
}
// Uso:
var robot = new Robot();
robot.OnCommandReceived += cmd => Console.WriteLine($"Il robot esegue: {cmd}");
robot.OnCommandReceived += cmd =>
{
if (cmd == "Accenditi")
Console.WriteLine("Avvio del sistema...");
};
// Proviamo
robot.ReceiveCommand("Accenditi");
robot.ReceiveCommand("Muoviti avanti");
Questo approccio è spesso usato in test, prototipi, in container DI (dependency injection) e per passare logica di business tramite parametri.
6. Delegati come sottoscrizioni allo stato
Supponiamo di avere una classe che mantiene uno stato e vogliamo notificare tutti i subscriber quando cambia. Con delegati (e eventi) è facilissimo.
Esempio 7: Classe con subscription al cambiamento
public class Notifier<T>
{
private T _value = default!;
public event Action<T>? ValueChanged;
public T Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
ValueChanged?.Invoke(_value);
}
}
}
}
// Uso:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Nuovo valore: {v}");
intValue.Value = 5; // Evento triggerato
intValue.Value = 10;
Questo approccio è praticamente "programmazione reattiva minimal", la base per MVVM, data binding e molti framework UI moderni.
7. Delegati, closure e scope lessicale
Le lambda e i metodi anonimi possono catturare variabili dal contesto esterno (closure). È comodo, ma a volte porta errori inaspettati.
Esempio 8: Cattura di variabile e la "trappola" del ciclo
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
a(); // Stamperà tre volte 3 (!)
Perché? Perché la closure fa riferimento alla stessa variabile i, che dopo il ciclo vale 3. Se vuoi fissare i valori 0, 1, 2?
for (int i = 0; i < 3; i++)
{
int loopValue = i; // "congeliamo" il valore corrente
actions[i] = () => Console.WriteLine(loopValue);
}
Ora il codice funziona come previsto. Queste trappole sono tra gli errori più comuni dei principianti con le lambda!
8. Combinazione di delegati
I delegati multicast contengono una lista di metodi, e puoi aggiungere (+=) o rimuovere (-=) handler.
Particolarità: la rimozione avviene per riferimento e signature
void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");
Action a = Handler1;
a += Handler2;
a -= Handler1; // Rimane solo Handler2
a?.Invoke(); // Mostra "2"
Esempio: gestione dinamica degli handler
Action a = Handler1;
a += Handler1;
a -= Handler1; // Ora rimane UN SOLO Handler1 nella lista!
9. Delegati, estendibilità e inversione del controllo (IoC)
In grandi applicazioni spesso serve che i componenti possano "chiamare" codice esterno rimanendo indipendenti. I delegati aiutano a inserire estensioni, plugin e callback senza tight coupling.
Esempio: Iniezione di comportamento nel costruttore
public class Greeter
{
private readonly Func<string> _getName;
public Greeter(Func<string> getName)
{
_getName = getName;
}
public void Greet() => Console.WriteLine($"Ciao, {_getName()}!");
}
// Iniezione di comportamenti diversi:
var greeter1 = new Greeter(() => "Anya");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());
greeter1.Greet(); // "Ciao, Anya!"
greeter2.Greet(); // "Ciao, 14:35!"
Nella pratica questo pattern è usato per scrivere codice testabile e manutenibile.
10. Sfumature utili
Delegati nelle interfacce standard e LINQ
Incontrerai delegati inevitabilmente se lavori con LINQ, collezioni e asincronia.
- Molti metodi come List<T>.Find, Array.Sort, Where, Select accettano delegati (Func<T, bool>, Comparison<T> ecc.).
- I metodi LINQ permettono di passare logica di filtro, trasformazione, aggregazione — senza creare classi separate.
Esempio: Comparator per ordinare oggetti
var people = new[] { "Ivan", "Maria", "Pietro" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Ivan, Pietro, Maria
Delegati e currying (applicazione parziale di argomenti)
Con metodi anonimi/lambda puoi "fissare" alcuni parametri e ottenere una nuova funzione.
Esempio: Applicazione parziale
Func<int, int, int> sum = (x, y) => x + y;
// Creiamo una funzione che aggiunge sempre 10
Func<int, int> add10 = y => sum(10, y);
Console.WriteLine(add10(5)); // 15
Particolarità del confronto tra delegati
In C# i delegati possono essere confrontati per uguaglianza (==), se hanno la stessa invocation list.
void Handler1() { }
void Handler2() { }
Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True
Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True
Ma se un delegato è costruito su un metodo anonimo o una lambda — vengono confrontate le istanze stesse.
Serializzazione dei delegati
I delegati si possono serializzare, ma solo se i metodi a cui puntano sono definiti in classi serializzabili e tutti i tipi sono disponibili. A partire da .NET 8, il BinaryFormatter è disabilitato di default ed è considerato obsoleto; nelle versioni future sarà rimosso del tutto; la serializzazione dei delegati in scenari di produzione è praticamente rara.
Interazione tra delegati ed eventi: dove usare l'uno o l'altro?
- Il delegato è un tipo/variabile che può essere invocata esplicitamente.
- L'evento (event) è un modo per limitare l'accesso al delegato: dall'esterno si può solo sottoscrivere/annullare la sottoscrizione (+=/-=), ma invocarlo solo dall'interno della classe.
- Un evento è sempre di tipo delegato, ma non ogni delegato è un evento.
Quando usare cosa? Se vuoi che la logica sia definita "fuori" dalla classe, usa delegati. Se vuoi controllare subscribe/unsubscribe e proteggere la variabile — usa un evento.
11. Errori tipici lavorando con i delegati
Errore №1: Confondere delegato ed evento.
Usare un campo delegato pubblico (public Action MyAction;) invece di un evento (public event Action MyAction;) rompe l'incapsulamento. Il codice esterno può accidentalmente o intenzionalmente sovrascrivere tutti i subscriber (instance.MyAction = null;) o invocarli direttamente, rompendo la logica della classe.
Errore №2: Gestione errata dei valori di ritorno nei delegati multicast.
Se un delegato ritorna un valore (per esempio Func<string, int>), con una chiamata normale (myDelegate("test")) verrà restituito solo il risultato dell'ultimo metodo nella catena. Per ottenere i risultati di tutti i subscriber, itera la lista di invocation tramite GetInvocationList().
// Esempio: iterare i risultati di tutti i subscriber
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
var r = ((Func<string, int>)d)("test");
Console.WriteLine(r);
}
Errore №3: Cattura della variabile del ciclo nella closure.
La classica trappola per i principianti: una lambda creata dentro un ciclo for cattura la variabile iteratore, non il suo valore corrente.
// Sbagliato: tutte le action stamperanno l'ultimo valore di i
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
// Giusto: creiamo una copia locale per ogni iterazione
for (int i = 0; i < 3; i++)
{
int copy = i;
actions[i] = () => Console.WriteLine(copy);
}
Errore №4: Creare memory leak.
Se un metodo di istanza si sottoscrive a un delegato di un oggetto long-lived e non si annulla la sottoscrizione, si crea una perdita di memoria. L'oggetto long-lived mantiene il riferimento al subscriber e il garbage collector non può rimuoverlo. Attenzione all'unsubscribe, specialmente in classi con ciclo di vita limitato (per esempio componenti UI).
GO TO FULL VERSION