Condividi tramite


System.Delegate e la delegate parola chiave

Precedente

Questo articolo illustra le classi in .NET che supportano i delegati e il modo in cui vengono mappati alla delegate parola chiave .

Che cosa sono i delegati?

Si pensi a un delegato come un modo per archiviare un riferimento a un metodo, in modo analogo a come archiviare un riferimento a un oggetto. Proprio come è possibile passare oggetti ai metodi, è possibile passare riferimenti al metodo usando delegati. Ciò è utile quando si vuole scrivere codice flessibile in cui i diversi metodi possono essere "collegati" per fornire comportamenti diversi.

Si supponga, ad esempio, di avere una calcolatrice in grado di eseguire operazioni su due numeri. Anziché inserire manualmente aggiunta, sottrazione, moltiplicazione e divisione in metodi separati, si possono usare delegati per rappresentare qualsiasi operazione che riceve due numeri e restituisce un risultato.

Definire i tipi di delegato

Si vedrà ora come creare tipi delegati usando la delegate parola chiave . Quando si definisce un tipo delegato, si sta essenzialmente creando un modello che descrive il tipo di metodi che può essere archiviato in tale delegato.

Si definisce un tipo delegato usando la sintassi simile a una firma del metodo, ma con la delegate parola chiave all'inizio:

// Define a simple delegate that can point to methods taking two integers and returning an integer
public delegate int Calculator(int x, int y);

Questo Calculator delegato può contenere riferimenti a qualsiasi metodo che accetta due int parametri e restituisce un oggetto int.

Di seguito è riportato un esempio più pratico. Quando si desidera ordinare un elenco, è necessario indicare all'algoritmo di ordinamento come confrontare gli elementi. Vediamo come i delegati aiutano con il metodo List.Sort(). Il primo passaggio consiste nel creare un tipo delegato per l'operazione di confronto:

// From the .NET Core library
public delegate int Comparison<in T>(T left, T right);

Questo Comparison<T> delegato può contenere riferimenti a qualsiasi metodo che:

  • Accetta due parametri di tipo T
  • Restituisce un valore int (in genere -1, 0 o 1 per indicare "minore di", "uguale a" o "maggiore di")

Quando si definisce un tipo delegato come questo, il compilatore genera automaticamente una classe derivata da System.Delegate che corrisponde alla firma. Questa classe gestisce tutta la complessità dell'archiviazione e della chiamata dei riferimenti ai metodi.

Il Comparison tipo delegato è un tipo generico, il che significa che può funzionare con qualsiasi tipo T. Per altre informazioni sui generics, vedere Classi e metodi generici.

Si noti che anche se la sintassi è simile alla dichiarazione di una variabile, si sta effettivamente dichiarando un nuovo tipo. È possibile definire tipi delegati all'interno di classi, direttamente all'interno degli spazi dei nomi o anche nello spazio dei nomi globale.

Annotazioni

Non è consigliabile dichiarare i tipi delegati (o altri tipi) direttamente nello spazio dei nomi globale.

Il compilatore genera anche gestori di aggiunta e rimozione per questo nuovo tipo in modo che i client di questa classe possano aggiungere e rimuovere metodi dall'elenco chiamate di un'istanza. Il compilatore impone che la firma del metodo aggiunto o rimosso corrisponda alla firma utilizzata durante la dichiarazione del tipo delegato.

Dichiarare istanze di delegati

Dopo aver definito il tipo delegato, è possibile creare istanze (variabili) di tale tipo. Immagina di creare uno "slot" in cui puoi memorizzare un riferimento a un metodo.

Analogamente a tutte le variabili in C#, non è possibile dichiarare istanze delegate direttamente in uno spazio dei nomi o nello spazio dei nomi globale.

// Inside a class definition:
public Comparison<T> comparator;

Il tipo di questa variabile è Comparison<T> (il tipo delegato definito in precedenza) e il nome della variabile è comparator. A questo punto, comparator non punta ancora ad alcun metodo: è come uno slot vuoto in attesa di essere riempito.

È anche possibile dichiarare variabili delegate come variabili locali o parametri del metodo, proprio come qualsiasi altro tipo di variabile.

Richiamare i delegati

Dopo aver creato un'istanza del delegato che punta a un metodo, è possibile chiamare (richiamare) tale metodo tramite il delegato. Richiami i metodi presenti nell'elenco di invocazione di un delegato chiamando quel delegato come se fosse un metodo.

Ecco come il metodo usa il Sort() delegato di confronto per determinare l'ordine degli oggetti:

int result = comparator(left, right);

In questa riga il codice richiama il metodo associato al delegato. La variabile delegato viene considerata come se fosse un nome di metodo e la si chiami usando la sintassi normale della chiamata al metodo.

Tuttavia, questa riga di codice fa un'ipotesi non sicura: dà per scontato che un metodo target sia stato aggiunto al delegato. Se non sono stati associati metodi, la riga precedente genererebbe un'eccezione NullReferenceException . I modelli usati per risolvere questo problema sono più sofisticati di un semplice controllo null e vengono trattati più avanti in questa serie.

Assegnare, aggiungere e rimuovere destinazioni di chiamata

Ora sai come definire i tipi di delegati, dichiarare le istanze dei delegati e richiamare i delegati. Ma come si connette effettivamente un metodo a un delegato? È qui che entra in gioco l'assegnazione del delegato.

Per usare un delegato, è necessario assegnarvi un metodo. Il metodo assegnato deve avere la stessa firma (stessi parametri e tipo restituito) definiti dal tipo delegato.

Vediamo un esempio pratico. Si supponga di voler ordinare un elenco di stringhe in base alla loro lunghezza. È necessario creare un metodo di confronto che corrisponda alla firma del Comparison<string> delegato:

private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);

Questo metodo accetta due stringhe e restituisce un numero intero che indica quale stringa è "maggiore" (più a lungo in questo caso). Il metodo viene dichiarato come privato, che è perfettamente corretto. Non è necessario che il metodo faccia parte dell'interfaccia pubblica per usarlo con un delegato.

È ora possibile passare questo metodo al List.Sort() metodo :

phrases.Sort(CompareLength);

Si noti che si usa il nome del metodo senza parentesi. Ciò indica al compilatore di convertire il riferimento al metodo in un delegato che può essere richiamato in un secondo momento. Il Sort() metodo chiamerà il CompareLength metodo ogni volta che deve confrontare due stringhe.

È anche possibile essere più espliciti dichiarando una variabile delegato e assegnando il metodo:

Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);

Entrambi gli approcci esegono la stessa cosa. Il primo approccio è più conciso, mentre il secondo rende più esplicita l'assegnazione del delegato.

Per i metodi semplici, è comune usare espressioni lambda anziché definire un metodo separato:

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);

Le espressioni lambda offrono un modo compatto per definire metodi semplici inline. L'uso di espressioni lambda per le destinazioni del delegato è descritto in modo più dettagliato in una sezione successiva.

Gli esempi finora mostrano i delegati con un singolo metodo di destinazione. Tuttavia, gli oggetti delegati possono supportare elenchi di chiamate che dispongono di più metodi di destinazione collegati a un singolo oggetto delegato. Questa funzionalità è particolarmente utile per gli scenari di gestione degli eventi.

Classi Delegate e MulticastDelegate

In background, le funzionalità del delegato in uso sono basate su due classi chiave in .NET Framework: Delegate e MulticastDelegate. In genere non si lavora direttamente con queste classi, ma forniscono le basi che permettono ai delegati di funzionare.

La System.Delegate classe e la sua sottoclasse diretta System.MulticastDelegate forniscono il supporto del framework per la creazione di delegati, la registrazione di metodi come target di delegati e l'invocazione di tutti i metodi registrati con un delegato.

Ecco un interessante dettaglio di progettazione: System.Delegate e System.MulticastDelegate non sono tipi delegati che è possibile usare. Fungono invece da classi di base per tutti i tipi delegati specifici creati. Il linguaggio C# impedisce di ereditare direttamente da queste classi. È invece necessario usare la delegate parola chiave .

Quando si usa la delegate parola chiave per dichiarare un tipo delegato, il compilatore C# crea automaticamente una classe derivata da MulticastDelegate con la firma specifica.

Perché questo design?

Questa progettazione ha le sue radici nella prima versione di C# e .NET. Il team di progettazione ha avuto diversi obiettivi:

  1. Sicurezza dei tipi: il team voleva assicurarsi che il linguaggio applicasse la sicurezza dei tipi quando si usano i delegati. Ciò significa garantire che i delegati vengano richiamati con il tipo e il numero di argomenti corretti e che i tipi restituiti vengano verificati correttamente in fase di compilazione.

  2. Prestazioni: se il compilatore genera classi delegate concrete che rappresentano firme di metodi specifiche, il runtime può ottimizzare le chiamate delegate.

  3. Semplicità: I delegati sono stati inclusi nella versione 1.0 di .NET, prima dell'introduzione dei generics (tipi generici). Progettazione necessaria per lavorare entro i vincoli del tempo.

La soluzione era quella di fare in modo che il compilatore creasse le classi delegate concrete che corrispondono alle tue firme del metodo, garantendo la sicurezza dei tipi e nascondendo al tempo stesso la complessità.

Lavorare con i metodi delegati

Anche se non è possibile creare direttamente classi derivate, si useranno occasionalmente metodi definiti nelle Delegate classi e MulticastDelegate . Ecco i più importanti da conoscere:

Ogni delegato con cui lavori è derivato da MulticastDelegate. Un delegato "multicast" indica che è possibile richiamare più di una destinazione del metodo durante la chiamata tramite un delegato. La progettazione originale ha considerato una distinzione tra delegati che potrebbero richiamare un solo metodo e delegati che potrebbero richiamare più metodi. In pratica, questa distinzione si è rivelata meno utile del pensiero originale, quindi tutti i delegati in .NET supportano più metodi di destinazione.

I metodi più comunemente impiegati quando si usano i delegati sono:

  • Invoke(): chiama tutti i metodi associati al delegato
  • BeginInvoke() / EndInvoke(): usato per i modelli di chiamata asincrona (anche se async/await ora è preferibile)

Nella maggior parte dei casi, questi metodi non verranno chiamati direttamente. Si userà invece la sintassi della chiamata al metodo nella variabile delegato, come illustrato negli esempi precedenti. Tuttavia, come si vedrà più avanti in questa serie, esistono modelli che funzionano direttamente con questi metodi.

Riassunto

Ora che hai visto come la sintassi del linguaggio C# mappa alle classi .NET sottostanti, puoi esplorare il modo in cui i delegati fortemente tipizzati vengono usati, creati e richiamati in scenari più complessi.

Avanti