CodeGym /Corsi /C# SELF /Delegati: scenari avanzati

Delegati: scenari avanzati

C# SELF
Livello 54 , Lezione 3
Disponibile

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).

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION