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 predefinito | Dichiarazione personalizzata equivalente |
|---|---|
Action | delegate 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.