20 delegati
20.1 Generale
Una dichiarazione delegato definisce una classe derivata dalla classe System.Delegate
. Un'istanza del delegato incapsula un elenco chiamate, ovvero un elenco di uno o più metodi, ognuno dei quali viene definito entità chiamabile. Per i metodi di istanza, un'entità chiamabile è costituita da un'istanza e da un metodo in tale istanza. Per i metodi statici, un'entità chiamabile è costituita solo da un metodo. Richiamando un'istanza del delegato con un set appropriato di argomenti, ogni entità chiamabile del delegato viene richiamata con il set specificato di argomenti.
Nota: una proprietà interessante e utile di un'istanza del delegato è che non conosce o si preoccupa delle classi dei metodi incapsulati; tutto ciò che conta è che questi metodi siano compatibili (§20.4) con il tipo del delegato. In questo modo, i delegati sono perfettamente adatti per la chiamata "anonima". nota finale
20.2 Dichiarazioni delegate
Un delegate_declaration è un type_declaration (§14.7) che dichiara un nuovo tipo delegato.
delegate_declaration
: attributes? delegate_modifier* 'delegate' return_type delegate_header
| attributes? delegate_modifier* 'delegate' ref_kind ref_return_type
delegate_header
;
delegate_header
: identifier '(' parameter_list? ')' ';'
| identifier variant_type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause* ';'
;
delegate_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| unsafe_modifier // unsafe code support
;
unsafe_modifier è definito in §23.2.
Si tratta di un errore in fase di compilazione per visualizzare più volte lo stesso modificatore in una dichiarazione di delegato.
Una dichiarazione di delegato che fornisce un variant_type_parameter_list è una dichiarazione delegato generica. Inoltre, qualsiasi delegato annidato all'interno di una dichiarazione di classe generica o una dichiarazione di struct generica è una dichiarazione delegato generica, poiché gli argomenti di tipo per il tipo contenitore devono essere forniti per creare un tipo costruito (§8.4).
Il new
modificatore è consentito solo sui delegati dichiarati all'interno di un altro tipo, nel qual caso specifica che tale delegato nasconde un membro ereditato con lo stesso nome, come descritto in §15.3.5.
I public
modificatori , protected
internal
, e private
controllano l'accessibilità del tipo delegato. A seconda del contesto in cui si verifica la dichiarazione del delegato, alcuni di questi modificatori potrebbero non essere consentiti (§7.5.2).
Il nome del tipo del delegato è l'identificatore.
Come per i metodi (§15.6.1), se ref
è presente, il delegato restituisce per riferimento; in caso contrario, se return_type è void
, il delegato restituisce no-value; in caso contrario, il delegato restituisce per valore.
Il parameter_list facoltativo specifica i parametri del delegato.
Il return_type di una dichiarazione di delegato returns-by-value o returns-no-value specifica il tipo del risultato, se presente, restituito dal delegato.
Il ref_return_type di una dichiarazione di delegato return-by-ref specifica il tipo della variabile a cui fa riferimento il variable_reference (§9.5) restituito dal delegato.
Il variant_type_parameter_list facoltativo (§18.2.3) specifica i parametri di tipo per il delegato stesso.
Il tipo restituito di un tipo delegato deve essere void
, o indipendente dall'output (§18.2.3.2).
Tutti i tipi di parametro di un tipo delegato devono essere indipendenti dall'input (§18.2.3.2). Inoltre, qualsiasi tipo di parametro di output o riferimento deve essere indipendente dall'output.
Nota: i parametri di output devono essere sicuri per l'input a causa di restrizioni di implementazione comuni. nota finale
Inoltre, ogni vincolo di tipo di classe, vincolo del tipo di interfaccia e vincolo di parametro di tipo su qualsiasi parametro di tipo del delegato deve essere indipendente dall'input.
I tipi delegati in C# sono equivalenti, non strutturalmente equivalenti.
Esempio:
delegate int D1(int i, double d); delegate int D2(int c, double d);
I tipi
D1
delegati eD2
sono due tipi diversi, quindi non sono intercambiabili, nonostante le firme identiche.esempio finale
Analogamente ad altre dichiarazioni di tipo generico, gli argomenti di tipo devono essere assegnati per creare un tipo delegato costruito. I tipi di parametro e il tipo restituito di un tipo delegato costruito vengono creati sostituendo, per ogni parametro di tipo nella dichiarazione del delegato, l'argomento di tipo corrispondente del tipo delegato costruito.
L'unico modo per dichiarare un tipo delegato è tramite un delegate_declaration. Ogni tipo delegato è un tipo riferimento derivato da System.Delegate
. I membri necessari per ogni tipo di delegato sono descritti in dettaglio in §20.3. I tipi delegati sono implicitamente sealed
, pertanto non è consentito derivare alcun tipo da un tipo delegato. Non è inoltre consentito dichiarare un tipo di classe non delegato derivato da System.Delegate
. System.Delegate
non è un tipo delegato; è un tipo di classe da cui derivano tutti i tipi delegati.
20.3 Membri delegati
Ogni tipo delegato eredita i membri dalla Delegate
classe come descritto in §15.3.4. Inoltre, ogni tipo delegato deve fornire un metodo non generico Invoke
il cui elenco di parametri corrisponde al parameter_list nella dichiarazione del delegato, il cui tipo restituito corrisponde al return_type o ref_return_type nella dichiarazione del delegato e per i delegati return-by-ref il cui ref_kind corrisponde a quello nella dichiarazione del delegato. Il Invoke
metodo deve essere almeno accessibile come il tipo delegato contenitore. La chiamata al Invoke
metodo su un tipo delegato equivale semanticamente all'uso della sintassi di chiamata del delegato (§20.6).
Le implementazioni possono definire membri aggiuntivi nel tipo delegato.
Ad eccezione della creazione di istanze, qualsiasi operazione che può essere applicata a una classe o a un'istanza di classe può essere applicata rispettivamente a una classe o a un'istanza del delegato. In particolare, è possibile accedere ai membri del System.Delegate
tipo tramite la sintassi di accesso ai membri consueta.
Compatibilità dei delegati 20.4
Un metodo o un tipo M
delegato è compatibile con un tipo D
delegato se sono soddisfatte tutte le condizioni seguenti:
D
eM
hanno lo stesso numero di parametri e ogni parametro inD
ha lo stesso modificatore di parametro by-reference del parametro corrispondente inM
.- Per ogni parametro di valore, esiste una conversione identity (§10.2.2) o una conversione implicita dei riferimenti (§10.2.8) dal tipo di parametro in
D
al tipo di parametro corrispondente inM
. - Per ogni parametro per riferimento, il tipo di parametro in
D
è uguale al tipo di parametro inM
. - Una delle condizioni seguenti è vera:
D
eM
sono entrambi returns-no-valueD
eM
sono return-by-value (§15.6.1, §20.2) e esiste una conversione identity o implicit reference dal tipo restituito diM
al tipo restituito diD
.D
eM
sono entrambi return-by-ref, esiste una conversione di identità tra il tipo restituito diM
e il tipo restituito diD
e entrambi hanno lo stesso ref_kind.
Questa definizione di compatibilità consente la covarianza nel tipo restituito e nella controvarianza nei tipi di parametro.
Esempio:
delegate int D1(int i, double d); delegate int D2(int c, double d); delegate object D3(string s); class A { public static int M1(int a, double b) {...} } class B { public static int M1(int f, double g) {...} public static void M2(int k, double l) {...} public static int M3(int g) {...} public static void M4(int g) {...} public static object M5(string s) {...} public static int[] M6(object o) {...} }
I metodi
A.M1
eB.M1
sono compatibili con entrambi i tipiD1
delegati eD2
, poiché hanno lo stesso tipo restituito e lo stesso elenco di parametri. I metodiB.M2
,B.M3
eB.M4
sono incompatibili con i tipiD1
delegati eD2
, poiché hanno tipi restituiti o elenchi di parametri diversi. I metodiB.M5
eB.M6
sono entrambi compatibili con il tipoD3
delegato .esempio finale
Esempio:
delegate bool Predicate<T>(T value); class X { static bool F(int i) {...} static bool G(string s) {...} }
Il metodo
X.F
è compatibile con il tipoPredicate<int>
delegato e il metodoX.G
è compatibile con il tipoPredicate<string>
delegato .esempio finale
Nota: il significato intuitivo della compatibilità dei delegati è che un metodo è compatibile con un tipo delegato se ogni chiamata del delegato può essere sostituita con una chiamata del metodo senza violare la sicurezza dei tipi, trattando parametri facoltativi e matrici di parametri come parametri espliciti. Ad esempio, nel codice seguente:
delegate void Action<T>(T arg); class Test { static void Print(object value) => Console.WriteLine(value); static void Main() { Action<string> log = Print; log("text"); } }
Il
Action<string>
tipo delegato perché qualsiasi chiamata di unAction<string>
delegato sarebbe anche una chiamata valida delSe ad esempio la firma del
Print(object value, bool prependTimestamp = false)
in , ilAction<string>
le regole di questa clausola.nota finale
20.5 Creazione di istanze del delegato
Un'istanza di un delegato viene creata da un delegate_creation_expression (§12.8.16.6), una conversione in un tipo delegato, una combinazione di delegati o la rimozione del delegato. L'istanza del delegato appena creata fa quindi riferimento a una o più di:
- Metodo statico a cui si fa riferimento nel delegate_creation_expression o
- L'oggetto di destinazione (che non può essere
null
) e il metodo di istanza a cui si fa riferimento nel delegate_creation_expression o - Un altro delegato (§12.8.16.6).
Esempio:
delegate void D(int x); class C { public static void M1(int i) {...} public void M2(int i) {...} } class Test { static void Main() { D cd1 = new D(C.M1); // Static method C t = new C(); D cd2 = new D(t.M2); // Instance method D cd3 = new D(cd2); // Another delegate } }
esempio finale
Il set di metodi incapsulati da un'istanza del delegato è denominato elenco chiamate. Quando un'istanza del delegato viene creata da un singolo metodo, incapsula tale metodo e il relativo elenco chiamate contiene una sola voce. Tuttavia, quando due istanze nonnull
delegate vengono combinate, i relativi elenchi di chiamate vengono concatenati, nell'ordine dell'operando sinistro e quindi a destra, per formare un nuovo elenco chiamate, che contiene due o più voci.
Quando un nuovo delegato viene creato da un singolo delegato, l'elenco chiamate risultante ha una sola voce, ovvero il delegato di origine (§12.8.16.6).
I delegati vengono combinati usando il file binario (§12.10.5) e +=
gli operatori (§12.21.4).+
Un delegato può essere rimosso da una combinazione di delegati, utilizzando il file binario -
(§12.10.6) e -=
gli operatori (§12.21.4). I delegati possono essere confrontati per l'uguaglianza (§12.12.9).
Esempio: l'esempio seguente mostra la creazione di un'istanza di un numero di delegati e gli elenchi di chiamate corrispondenti:
delegate void D(int x); class C { public static void M1(int i) {...} public static void M2(int i) {...} } class Test { static void Main() { D cd1 = new D(C.M1); // M1 - one entry in invocation list D cd2 = new D(C.M2); // M2 - one entry D cd3 = cd1 + cd2; // M1 + M2 - two entries D cd4 = cd3 + cd1; // M1 + M2 + M1 - three entries D cd5 = cd4 + cd3; // M1 + M2 + M1 + M1 + M2 - five entries D td3 = new D(cd3); // [M1 + M2] - ONE entry in invocation // list, which is itself a list of two methods. D td4 = td3 + cd1; // [M1 + M2] + M1 - two entries D cd6 = cd4 - cd2; // M1 + M1 - two entries in invocation list D td6 = td4 - cd2; // [M1 + M2] + M1 - two entries in invocation list, // but still three methods called, M2 not removed. } }
Quando
cd1
ecd2
vengono create istanze, ognuna incapsula un metodo. Quandocd3
viene creata un'istanza, ha un elenco chiamate di due metodi,M1
eM2
, in tale ordine.cd4
L'elenco chiamate contieneM1
,M2
eM1
, in tale ordine. Percd5
, l'elenco chiamate contieneM1
,M2
,M1
,M1
eM2
, in tale ordine.Quando si crea un delegato da un altro delegato con un delegate_creation_expression il risultato ha un elenco chiamate con una struttura diversa dall'originale, ma che comporta la chiamata degli stessi metodi nello stesso ordine. Quando
td3
viene creato dalcd3
relativo elenco chiamate ha un solo membro, ma tale membro è un elenco dei metodi eM2
tali metodiM1
vengono richiamatitd3
nello stesso ordine in cui vengono richiamati dacd3
. Analogamente, quandotd4
viene creata un'istanza dell'elenco chiamate contiene solo due voci, ma richiama i tre metodiM1
,M2
eM1
, in tale ordine esattamente comecd4
avviene.La struttura dell'elenco chiamate influisce sulla sottrazione del delegato. Delegato
cd6
, creato sottraendocd2
(che richiamaM2
) dacd4
(che richiamaM1
,M2
eM1
) richiamaM1
eM1
. Tuttavia, delegatotd6
, creato sottraendocd2
(che richiamaM2
) datd4
(che richiamaM1
,M2
eM1
) richiamaM1
ancora ,M2
eM1
, in tale ordine, comeM2
non è una singola voce nell'elenco, ma un membro di un elenco annidato. Per altri esempi di combinazione (e rimozione) di delegati, vedere §20.6.esempio finale
Una volta creata un'istanza di un delegato, un'istanza del delegato fa sempre riferimento allo stesso elenco chiamate.
Nota: tenere presente che, quando due delegati vengono combinati o uno viene rimosso da un altro, un nuovo delegato restituisce un elenco di chiamate personalizzato. Gli elenchi di chiamate dei delegati combinati o rimossi rimangono invariati. nota finale
20.6 Chiamata del delegato
C# fornisce una sintassi speciale per richiamare un delegato. Quando viene richiamata un'istanza nonnull
delegato il cui elenco chiamate contiene una voce, richiama il metodo con gli stessi argomenti specificati e restituisce lo stesso valore del metodo indicato. Per informazioni dettagliate sulla chiamata del delegato, vedere §12.8.9.4 . Se si verifica un'eccezione durante la chiamata di tale delegato e tale eccezione non viene intercettata all'interno del metodo richiamato, la ricerca di una clausola catch di eccezione continua nel metodo che ha chiamato il delegato, come se tale metodo avesse chiamato direttamente il metodo a cui fa riferimento tale delegato.
Chiamata di un'istanza del delegato il cui elenco chiamate contiene più voci, procede richiamando ognuno dei metodi nell'elenco chiamate, in modo sincrono, in ordine. Ogni metodo così chiamato viene passato allo stesso set di argomenti assegnato all'istanza del delegato. Se una chiamata a tale delegato include parametri di riferimento (§15.6.2.3.3), ogni chiamata al metodo verrà eseguita con un riferimento alla stessa variabile. Le modifiche apportate a tale variabile da un metodo nell'elenco chiamate saranno visibili ai metodi più avanti nell'elenco chiamate. Se la chiamata del delegato include parametri di output o un valore restituito, il valore finale proviene dalla chiamata dell'ultimo delegato nell'elenco. Se si verifica un'eccezione durante l'elaborazione della chiamata di tale delegato e tale eccezione non viene intercettata all'interno del metodo richiamato, la ricerca di una clausola catch di eccezione continua nel metodo che ha chiamato il delegato e tutti i metodi più in basso nell'elenco chiamate non vengono richiamati.
Il tentativo di richiamare un'istanza del delegato il cui valore viene null
generato in un'eccezione di tipo System.NullReferenceException
.
Esempio: l'esempio seguente illustra come creare un'istanza, combinare, rimuovere e richiamare delegati:
delegate void D(int x); class C { public static void M1(int i) => Console.WriteLine("C.M1: " + i); public static void M2(int i) => Console.WriteLine("C.M2: " + i); public void M3(int i) => Console.WriteLine("C.M3: " + i); } class Test { static void Main() { D cd1 = new D(C.M1); cd1(-1); // call M1 D cd2 = new D(C.M2); cd2(-2); // call M2 D cd3 = cd1 + cd2; cd3(10); // call M1 then M2 cd3 += cd1; cd3(20); // call M1, M2, then M1 C c = new C(); D cd4 = new D(c.M3); cd3 += cd4; cd3(30); // call M1, M2, M1, then M3 cd3 -= cd1; // remove last M1 cd3(40); // call M1, M2, then M3 cd3 -= cd4; cd3(50); // call M1 then M2 cd3 -= cd2; cd3(60); // call M1 cd3 -= cd2; // impossible removal is benign cd3(60); // call M1 cd3 -= cd1; // invocation list is empty so cd3 is null // cd3(70); // System.NullReferenceException thrown cd3 -= cd1; // impossible removal is benign } }
Come illustrato nell'istruzione
cd3 += cd1;
, un delegato può essere presente in un elenco chiamate più volte. In questo caso, viene semplicemente richiamato una volta per occorrenza. In un elenco chiamate come questo, quando tale delegato viene rimosso, l'ultima occorrenza nell'elenco chiamate è quella effettivamente rimossa.Immediatamente prima dell'esecuzione dell'istruzione finale,
cd3 -= cd1
;, il delegatocd3
fa riferimento a un elenco di chiamate vuoto. Il tentativo di rimuovere un delegato da un elenco vuoto (o rimuovere un delegato inesistente da un elenco non vuoto) non è un errore.L'output prodotto è:
C.M1: -1 C.M2: -2 C.M1: 10 C.M2: 10 C.M1: 20 C.M2: 20 C.M1: 20 C.M1: 30 C.M2: 30 C.M1: 30 C.M3: 30 C.M1: 40 C.M2: 40 C.M3: 40 C.M1: 50 C.M2: 50 C.M1: 60 C.M1: 60
esempio finale
ECMA C# draft specification