Utilizzo di delegati (Guida per programmatori C#)

Un delegato è un tipo che incapsula in modo sicuro un metodo, simile a un puntatore a funzione in C e C++. A differenza dei puntatori a funzione, tuttavia, i delegati sono orientati a oggetti, indipendenti dai tipi e sicuri. Il tipo delegato è definito dal nome del delegato. Nell'esempio seguente viene dichiarato un delegato denominato Callback che può incapsulare un metodo che accetta una stringa come argomento e restituisce void:

public delegate void Callback(string message);

Un oggetto delegato viene in genere costruito specificando il nome del metodo che il delegato eseguirà il wrapping o con un'espressione lambda. Una volta creata un'istanza di un delegato in questo modo, può essere richiamata. Richiamare un delegato chiama il metodo associato all'istanza del delegato. I parametri passati al delegato dal chiamante vengono passati al metodo e il valore restituito, se presente, dal metodo viene restituito al chiamante dal delegato. Ad esempio:

// Create a method for a delegate.
public static void DelegateMethod(string message)
{
    Console.WriteLine(message);
}
// Instantiate the delegate.
Callback handler = DelegateMethod;

// Call the delegate.
handler("Hello World");

I tipi delegati sono derivati dalla Delegate classe in .NET. I tipi delegati sono sealed, ovvero non possono essere usati per la derivazione, e non è possibile derivare classi personalizzate da Delegate. Poiché il delegato di cui è stata creata un'istanza è un oggetto, può essere passato come argomento o assegnato a una proprietà. In questo modo un metodo può accettare un delegato come parametro e chiamare il delegato in un secondo momento. Questa operazione è nota come callback asincrono ed è un metodo comune per notificare un chiamante al termine di un processo lungo. Quando un delegato viene usato in questo modo, per il codice che usa il delegato non è richiesta alcuna conoscenza dell'implementazione del metodo in uso. La funzionalità è simile all'incapsulamento fornito dalle interfacce.

Un altro utilizzo comune dei callback è la definizione di un metodo di confronto personalizzato e il passaggio di tale delegato a un metodo di ordinamento. Consente al codice del chiamante di entrare a far parte dell'algoritmo di ordinamento. Nell'esempio di metodo seguente viene usato il tipo Del come parametro:

public static void MethodWithCallback(int param1, int param2, Callback callback)
{
    callback("The number is: " + (param1 + param2).ToString());
}

È quindi possibile passare il delegato creato in precedenza a tale metodo:

MethodWithCallback(1, 2, handler);

e visualizzare il seguente output sulla console:

The number is: 3

Usando il delegato come astrazione, non è necessario chiamare direttamente la console da MethodWithCallback, ovvero questo non deve essere progettato tenendo presente una console. MethodWithCallback si limita a preparare una stringa e a passarla a un altro metodo. Questa operazione è particolarmente efficace perché un metodo delegato può usare qualsiasi numero di parametri.

Quando viene creato un delegato per eseguire il wrapping di un metodo di istanza, il delegato fa riferimento sia all'istanza sia al metodo. Un delegato non ha alcuna conoscenza del tipo di istanza a parte il metodo di cui esegue il wrapping, perciò un delegato può fare riferimento a qualsiasi tipo di oggetto a condizione che vi sia un metodo su tale oggetto che corrisponda alla firma del delegato. Quando viene creato un delegato per eseguire il wrapping di un metodo statico, fa riferimento solo al metodo. Si considerino le dichiarazioni seguenti:

public class MethodClass
{
    public void Method1(string message) { }
    public void Method2(string message) { }
}

Insieme al metodo statico DelegateMethod illustrato in precedenza, ci sono tre metodi di cui è possibile eseguire il wrapping in un'istanza di Del.

Un delegato può chiamare più di un metodo, quando viene richiamato. Questo processo viene definito multicasting. Per aggiungere un ulteriore metodo all'elenco dei metodi del delegato (l'elenco chiamate), è necessario semplicemente aggiungere due delegati usando gli operatori addizione o di assegnazione di addizione ("+" o "+="). Ad esempio:

var obj = new MethodClass();
Callback d1 = obj.Method1;
Callback d2 = obj.Method2;
Callback d3 = DelegateMethod;

//Both types of assignment are valid.
Callback allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;

A questo punto allMethodsDelegate contiene tre metodi nel relativo elenco chiamate: Method1, Method2 e DelegateMethod. I tre delegati originali, d1, d2 e d3, rimangono invariati. Quando si richiama allMethodsDelegate, tutti e tre i metodi vengono chiamati nell'ordine. Se il delegato usa parametri per riferimento, il riferimento viene passato in sequenza a ciascuno dei tre metodi a turno e le eventuali modifiche apportate da un solo metodo saranno visibili al metodo successivo. Quando uno dei metodi genera un'eccezione non rilevata all'interno del metodo, tale eccezione viene passata al chiamante del delegato e non verrà chiamato nessun metodo successivo nell'elenco chiamate. Se il delegato ha un valore restituito e/o i parametri out, restituisce il valore restituito e i parametri dell'ultimo metodo richiamato. Per rimuovere un metodo dall'elenco chiamate, usare gli operatori di assegnazione di sottrazione o sottrazione (- o -=). Ad esempio:

//remove Method1
allMethodsDelegate -= d1;

// copy AllMethodsDelegate while removing d2
Callback oneMethodDelegate = allMethodsDelegate - d2;

Poiché i tipi delegati vengono derivati da System.Delegate, i metodi e le proprietà definiti da tale classe possono essere chiamati sul delegato. Ad esempio, per trovare il numero di metodi nell'elenco chiamate di un delegato, è possibile scrivere:

int invocationCount = d1.GetInvocationList().GetLength(0);

I delegati con più metodi nel relativo elenco chiamate derivano da MulticastDelegate, cioè una sottoclasse di System.Delegate. Il codice sopra riportato funziona in entrambi i casi, perché entrambe le classi supportano GetInvocationList.

I delegati multicast vengono ampiamente usati nella gestione degli eventi. Gli oggetti di origine evento inviano notifiche di eventi agli oggetti destinatario registrati per ricevere l'evento. Per registrarsi a un evento, il destinatario crea un metodo che può gestire l'evento, quindi crea un delegato per il metodo e passa il delegato all'origine evento. L'origine chiama il delegato quando si verifica l'evento. Il delegato chiama quindi il metodo di gestione eventi sul destinatario, recapitando i dati dell'evento. Il tipo delegato per un determinato evento è definito dall'origine evento. Per altre informazioni, vedere Eventi.

Se si confrontano delegati di due tipi diversi assegnati in fase di compilazione si avrà un errore di compilazione. Se le istanze dei delegati sono staticamente del tipo System.Delegate, il confronto è consentito, ma restituirà false in fase di esecuzione. Ad esempio:

delegate void Callback1();
delegate void Callback2();

static void method(Callback1 d, Callback2 e, System.Delegate f)
{
    // Compile-time error.
    //Console.WriteLine(d == e);

    // OK at compile-time. False if the run-time type of f
    // is not the same as that of d.
    Console.WriteLine(d == f);
}

Vedi anche