Menu

Delegate in C#: dichiarazione, invocazione, multicast e callback

Cos'è un delegate in C# e come si usa: dichiarare un tipo delegate, crearlo da un metodo, da una lambda o da un metodo anonimo, invocarlo, i delegate come callback, i delegate multicast con += e -=, il controllo su null con ?.Invoke e il rapporto tra delegate, Func, Action ed eventi.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un delegate è un tipo che rappresenta la firma di un metodo. Una variabile di tipo delegate contiene un riferimento a un metodo con quella firma, e chiamare la variabile chiama il metodo. È così che C# tratta le funzioni come valori: puoi memorizzarle, passarle ad altri metodi e chiamarle in seguito.

Output:

40
70
68.00

delegate decimal PriceRule(decimal price); dichiara un nuovo tipo, allo stesso modo di class. Qualsiasi metodo che riceve un decimal e restituisce un decimal è compatibile. La variabile rule ha puntato a turno a tre metodi diversi, e ogni chiamata ha eseguito quello che conteneva in quel momento.

Dichiarare, creare e invocare

La dichiarazione di un tipo delegate somiglia alla firma di un metodo con la parola chiave delegate davanti. Di solito si trova a livello di namespace, accanto alle classi, oppure annidata dentro una classe.

Ci sono quattro modi per creare un'istanza di delegate:

PriceRule a = HalfPrice;                             // method group (most common)
PriceRule b = new PriceRule(HalfPrice);              // explicit constructor, same thing
PriceRule c = p => p - 5m;                            // lambda expression
PriceRule d = delegate (decimal p) { return p; };    // anonymous method (C# 2 syntax)

E due modi per chiamarla, identici tra loro: rule(80m) e rule.Invoke(80m). Il compilatore verifica la firma al momento dell'assegnazione, quindi un metodo non compatibile è un errore di compilazione invece di una sorpresa a runtime.

Un delegate può puntare anche a un metodo di istanza. In quel caso ricorda sia il metodo sia l'oggetto su cui chiamarlo:

Output:

Ana got: Your order shipped
target: Mailbox, method: Receive

Target è l'oggetto su cui il delegate chiamerà il metodo (null per un metodo statico), e Method descrive il metodo. Tenere il target significa anche che un delegate mantiene in vita quell'oggetto, cosa che conta per gli eventi.

I delegate come callback

Il compito principale di un delegate è permettere a un metodo di chiamare codice scelto da chi lo chiama. Il metodo definisce quando succede qualcosa; il chiamante definisce cosa:

Output:

[##        ] 25%
[#####     ] 50%
[#######   ] 75%
[##########] 100%
import finished

ImportRows non sa nulla di console o di barre. Lo stesso metodo può aggiornare una barra di avanzamento, una riga di log o una UI, a seconda di cosa passa il chiamante. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) e ogni operatore LINQ seguono questo pattern.

Delegate multicast: += e -=

Un delegate può contenere più di un metodo. += aggiunge un metodo alla sua lista di invocazione, -= lo rimuove, e invocare il delegate chiama ogni metodo in ordine:

Output:

email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002

Tre dettagli dei delegate multicast contano nella pratica:

  • Valori di ritorno: se il tipo delegate restituisce un valore, invocare un delegate multicast restituisce solo il risultato dell'ultimo metodo. Per raccogliere tutti i risultati, scorri GetInvocationList() con un ciclo e invoca tu ciascun elemento.
  • Eccezioni: se un metodo lancia un'eccezione, i metodi restanti non vengono eseguiti e l'eccezione arriva al chiamante.
  • Immutabilità: i delegate sono immutabili. += crea un nuovo delegate con una lista più lunga e lo riassegna alla variabile, ed è per questo che rimuovere con -= su una copia non influisce sull'originale.

Rimuovere una lambda con -= funziona solo se passi la stessa istanza di delegate. Scrivere due volte la stessa lambda crea due delegate diversi, quindi handlers -= id => Log(id); non rimuove nulla. Memorizza la lambda in una variabile se prevedi di annullare la sottoscrizione.

Delegate null e ?.Invoke

Una variabile delegate a cui non è stato assegnato nulla è null, e anche rimuovere l'ultimo metodo con -= lascia null. Invocare null lancia NullReferenceException:

Output:

no handler, no crash
saved draft.txt
NullReferenceException

?.Invoke è il modo standard per chiamare un callback facoltativo. L'operatore ?. non può essere messo direttamente prima delle parentesi (onSaved?("x") non è una sintassi valida), quindi il pattern chiama per nome il metodo Invoke del delegate, che fa la stessa cosa della chiamata abbreviata.

Delegate, Func, Action ed eventi

Dichiarare un tipo delegate per ogni firma sarebbe noioso, quindi .NET ne fornisce di generici:

Tipo predefinitoDichiarazione personalizzata equivalente
Actiondelegate void Action();
Action<T>delegate void Action<T>(T arg);
Func<TResult>delegate TResult Func<TResult>();
Func<T, TResult>delegate TResult Func<T, TResult>(T arg);
Predicate<T>delegate bool Predicate<T>(T obj);

PriceRule del primo esempio ha la stessa forma di Func<decimal, decimal>, eppure i due sono tipi distinti e non si convertono l'uno nell'altro. Nemmeno due tipi predefiniti con firme identiche lo fanno:

Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'

Predicate<int> ok = new Predicate<int>(isPositive);   // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x);              // or with a lambda

Una lambda si converte in qualsiasi tipo delegate compatibile, quindi le API di solito ricevono lambda e il problema non si pone mai. La pagina su Func e Action tratta la famiglia generica e quando una dichiarazione personalizzata vale ancora la pena.

Gli eventi sono costruiti direttamente sui delegate. Un campo event è un delegate multicast su cui il codice esterno può solo fare += e -=, mai invocarlo né sovrascriverlo. La lista di sottoscrittori dell'esempio multicast qui sopra è esattamente ciò che un evento gestisce per te, con quella protezione in più. Gli eventi hanno una pagina dedicata.

Domande frequenti

Cos'è un delegate in C#?

Un delegate è un riferimento type-safe a un metodo. Dichiari un tipo delegate con una firma, come delegate decimal PriceRule(decimal price);, e qualsiasi metodo con quella firma (statico, di istanza o una lambda) può essere memorizzato in una variabile di quel tipo e chiamato in seguito tramite essa. I delegate sono il modo in cui C# passa le funzioni come valori.

Che differenza c'è tra un delegate e Func in C#?

Func<T, TResult> e Action<T> sono tipi delegate che .NET dichiara già per te, generici sui tipi dei parametri e di ritorno. Una dichiarazione delegate personalizzata dà al tipo un nome e dei nomi di parametro che ne documentano il ruolo, e supporta parametri ref, out e in. Per la maggior parte del codice, Func e Action bastano.

Cos'è un delegate multicast in C#?

Un delegate che contiene più metodi. += aggiunge un metodo alla sua lista di invocazione e -= ne rimuove uno; invocare il delegate li chiama tutti in ordine. Se il delegate restituisce un valore, il chiamante riceve solo il risultato dell'ultimo metodo, e un'eccezione in un metodo impedisce l'esecuzione dei restanti.

Come invoco in sicurezza un delegate che potrebbe essere null?

Usa l'operatore null-condizionale: onProgress?.Invoke(50);. Una variabile delegate a cui non è stato assegnato alcun metodo è null, e chiamarla direttamente lancia NullReferenceException. ?.Invoke legge la variabile una sola volta, quindi è sicuro anche se un altro thread rimuove l'ultimo handler nello stesso momento.

Quando dichiarare un tipo delegate personalizzato invece di usare Func o Action?

Quando la firma ha bisogno di parametri ref, out o in (Func e Action non possono esprimerli), quando un tipo con un nome rende più chiara un'API pubblica (delegate bool Validator(string input)), o quando vuoi che i nomi dei parametri compaiano in IntelliSense. Altrimenti preferisci Func e Action, che ogni sviluppatore .NET riconosce.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA