Condividi tramite


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 publicmodificatori , protectedinternal, 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 e D2 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 e M hanno lo stesso numero di parametri e ogni parametro in D ha lo stesso modificatore di parametro by-reference del parametro corrispondente in M.
  • 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 in M.
  • Per ogni parametro per riferimento, il tipo di parametro in D è uguale al tipo di parametro in M.
  • Una delle condizioni seguenti è vera:
    • D e M sono entrambi returns-no-value
    • D e M sono return-by-value (§15.6.1, §20.2) e esiste una conversione identity o implicit reference dal tipo restituito di M al tipo restituito di D.
    • D e M sono entrambi return-by-ref, esiste una conversione di identità tra il tipo restituito di M e il tipo restituito di De 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 e B.M1 sono compatibili con entrambi i tipi D1 delegati e D2, poiché hanno lo stesso tipo restituito e lo stesso elenco di parametri. I metodi B.M2, B.M3e B.M4 sono incompatibili con i tipi D1 delegati e D2, poiché hanno tipi restituiti o elenchi di parametri diversi. I metodi B.M5 e B.M6 sono entrambi compatibili con il tipo D3delegato .

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 tipo Predicate<int> delegato e il metodo X.G è compatibile con il tipo Predicate<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 Print metodo è compatibile con il Action<string> tipo delegato perché qualsiasi chiamata di un Action<string> delegato sarebbe anche una chiamata valida del Print metodo .

Se ad esempio la firma del Print metodo precedente è stata modificata Print(object value, bool prependTimestamp = false) in , il Print metodo non sarà più compatibile con Action<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 e cd2 vengono create istanze, ognuna incapsula un metodo. Quando cd3 viene creata un'istanza, ha un elenco chiamate di due metodi, M1 e M2, in tale ordine. cd4L'elenco chiamate contiene M1, M2e M1, in tale ordine. Per cd5, l'elenco chiamate contiene M1, M2, M1, M1e M2, 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 dal cd3 relativo elenco chiamate ha un solo membro, ma tale membro è un elenco dei metodi e M2 tali metodi M1 vengono richiamati td3 nello stesso ordine in cui vengono richiamati da cd3. Analogamente, quando td4 viene creata un'istanza dell'elenco chiamate contiene solo due voci, ma richiama i tre metodi M1, M2e M1, in tale ordine esattamente come cd4 avviene.

La struttura dell'elenco chiamate influisce sulla sottrazione del delegato. Delegato cd6, creato sottraendo cd2 (che richiama M2) da cd4 (che richiama M1, M2e M1) richiama M1 e M1. Tuttavia, delegato td6, creato sottraendo cd2 (che richiama M2) da td4 (che richiama M1, M2e M1) richiama M1ancora , M2 e M1, in tale ordine, come M2 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 delegato cd3 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