1. Introduzione

La parte introduttiva dellโ€™articolo รจ presa direttamente dala documentazione ufficiale MSDN.


Un delegate รจ un tipo che incapsula 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.

Un delegate deve essere usato per comunicare allโ€™utilizzatore della classe sviluppata: โ€œSentiti libero di usare qualsiasi metodo che rispetta la firma che ti fornisco e questo verrร  chiamato correttamenteโ€.

I delegate sono utili per offrire allโ€™utilizzatore dei miei oggetti lโ€™abilitร  di personalizzarne il comportamento, separando in ogni caso la classe dal suo utilizzatore.

Spesso รจ possibile ottenere lo stesso comportamento con modalitร  alternative allโ€™utilizzo dei delegate (un esempio รจ lโ€™utilizzo del pattern Strategy), ma spesso questi sono il modo piรน semplice e pulito per ottenere questi comportamenti.

Nellโ€™esempio seguente viene dichiarato un delegato denominato Del che puรฒ incapsulare un metodo che accetta una stringa come argomento e restituisce void:

public delegate void Del(string message);

Un oggetto delegato viene normalmente creato in due modi:

  • fornendo il nome del metodo di cui il delegato eseguirร  il wrapping
  • con un metodo anonimo (funzioni lambda)

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.

Se creasi una funzione con la stessa firma del delegato, posso richiamarla in questo modo:

//Stessa firma del delegate
public static void DelegateMethod(string message){
System.Console.WriteLine(message);
}
 
// Istanzio un oggetto di tipo Del con la funzione sopra
Del handler = DelegateMethod;
 
// Chiamo la funzione
handler("Hello World");

Poichรฉ lโ€™istanza del delegato รจ un oggetto, puรฒ essere passata come parametro o assegnata 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 void MethodWithCallback(int param1, int param2, Del callback)
{
  callback("The number is: " + (param1 + param2).ToString());
}

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

MethodWithCallback(1, 2, handler);

E ottenere come risultato:

The number is: 3

2. Multicasting

Assumiamo di aver definito la seguente classe

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

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:

MethodClass obj = new MethodClass();
Del d1 = obj.Method1;
Del d2 = obj.Method2;
Del d3 = DelegateMethod;
 
//Both types of assignment are valid.
 
Del allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;

A questo punto allMethodsDelegate contiene tre metodi nel relativo elenco chiamate: Method1, Method2 e DelegateMethod.

Quando si richiama allMethodsDelegate, tutti e tre i metodi vengono chiamati nellโ€™ordine.

Se il delegato usa parametri di 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.

Per rimuovere un metodo dallโ€™elenco chiamate, usare lโ€™operatore di decremento o lโ€™operatore di decremento di assegnazione (โ€-โ€ o โ€-=โ€โ€™). Ad esempio:

//remove Method1
allMethodsDelegate -= d1;
 
// copy AllMethodsDelegate while removing d2
Del oneMethodDelegate = allMethodsDelegate - d2;

I delegati multicast vengono ampiamente usati nella gestione degli eventi.

3. Metodi anonimi ed espressioni lambda

(questa sezione proviene da questo post) Di solito per assegnare una funzione a un delegato si scrive la funzione separatamente e si assegna al delegato il nome della funzione, invece con i metodi anonimi si puรฒ assegnare direttamente la funzione stessa, ecco un esempio:

delegate int MyDelegate(int a);
...
 
MyDelegate d = delegate(int a) { return a * 10; }

In questo modo non รจ necessario creare un metodo apposta. I metodi anonimi possono essere utili in vari casi ad esempio per passare la funzione di avvio di un thread.

Thread t = new Thread(
  delegate()
  {
    thread code ... ;
  });
  t.Start();

Una particolare forma di metodo anonimo sono le lambda expression utilizzate ad esempio in LINQ.

Nelle espressioni lambda compare lโ€™ operatore lambda "โ‡’" che si legge โ€œfino aโ€, alla sua sinistra vi sono i parametri di input (possono mancare) e alla sua destra il codice del metodo, che spesso si riduce ad una espressione, ma puรฒ essere complesso a piacere, quindi:

parametri => metodo

Ora assumiamo di aver definito il seguente delegato:

delegate int MyFunc(int i);

Questo puรฒ essere inizializzato con due modalitร , un metodo anonimo o una espressione lambda:

// Esempio con metodo anonimo:
MyFunc p = delegate(int i) { return i * 10; }
 
// La stessa cosa con una lambda expression
MyFunc p = i => i* 10;

In questo caso il compilatore deduce il tipo di โ€˜iโ€™ e del valore di ritorno in base al tipo del delegato.

Qualora via siano piรน parametri scrivo

(x, y) => x + y;

Si possono specificare anche i tipi dei parametri in ingresso quando il compilatore non li puรฒ dedurre:

(double x, string s) => s.Length + x;

Infine quando non vi sono parametri si usano le parentesi vuote:

() => DateTime.Now.Year % 2000;

Se invece il metodo contiene piรน parametri devo racchiuderlo tra parentesi graffe:

p => { int a = p * 2; return a + 1; }

4. I delegate e i generics

Essendo i delegate delle classi, รจ possibile sfruttare i generics per creare delle strutture facilmente riutilizzabili e versatili.

Allโ€™interno del framework .NET sono presenti un gran numero di delegate generici giร  fatti da poter riutilizzare, in particolare seguenti:

  • Action: Action<TParameter>: un delegate che prende da 0 a 8 parametri in ingresso e non ritorna nulla
  • Func: Func<TParameter, TResult>: un delegate che prende da 0 a 8 parametri in ingresso e che ritorna un valore o una referenza (di tipo TResult)
  • Predicate: รจ un wrapper di Func<T, bool>, viene usata per il compare di oggetti.

Analizziamoli uno ad uno.+

4.1 Func<TParameter, TOutput>

Func รจ logicamente simile allโ€™implementazione base dei delegate. La differenza รจ il modo in cui questa viene dichiarata:

Func<string, int, int> tempFuncPointer;

I primi due parametri sono i parametri in ingresso del metodo, mentre lโ€™ultimo parametro รจ un parametro in out che deve essere il tipo di dato di ritorno dal metodo.

Func<string, int, int> tempFuncPointer = tempObj.FirstTestFunction;
int value = tempFuncPointer("hello", 3);

4.2 Action<TParameter>

Action รจ usato quando la funzione non ha parametri in uscita.

Action<string, int> tempActionPointer;

Analogamente a Func, i parametri indicati sono i parametri in ingresso alla funzione, con la differenza che questa non ne ritorna nessuno.

Action<string, int> tempActionPointer = tempObj.ThirdTestFunction;
tempActionPointer("hello", 4);

4.3 Predicate<in T>

Predicate รจ un tipo Func che ritorna un valore booleano. Eโ€™ spesso usato nellโ€™analizi di liste di oggetti.

La dichiarazione รจ la seguente

Predicate<Employee> tempPredicatePointer;

in questo caso la funzione tempPredicatePointer prende in ingresso un oggetto Employee e ritorna true se Employee.age < 27:

public bool FourthTestFunction(Employee employee)
{
  return employee.Age < 27;
}

5. Delegate come interfacce anonime

La seguente idea proviene da questo post di Mark Seemann.


I delegate possono essere visti a tutti gli effetti come interfacce anonime formate da un solo metodo.

Consideriamo un semplice esempio, ho la classe MyClass che che possiede un metodo DoStuff. Questo metodo prende in ingresso un oggetto astratto (IMyInterface) che deve avere il metodo DoIt il quale, data una stringa, fornisce un intero.

Nella programmazione ad oggetti classica abbiamo:

public interface IMyInterface
{
  int DoIt(string message);
}
public string DoStuff(IMyInterface strategy)
{
  return strategy.DoIt("Ploeh").ToString();
}

Ma definire una nuova interfaccia solo per fare questo non รจ necessario, possiamo utilizzare il tipo Func<string, int> nel seguente modo:

public string DoStuff(Func<string, int> strategy)
{
  return strategy("Ploeh").ToString();
}

Questo metodo ci risparmia sia di definire una nuova interfaccia, che di implementare tale interfaccia per definire il metodo DoStuff.

Possiamo invece definire il codice scritto sopra con una lambda function:

string result = sut.DoStuff(s => s.Count());

Ovviamente questa tecnica funziona bene quando ho lโ€™astrazione di un singolo metodo: appena la mia struttura prevede lโ€™introduzione di un secondo metodo, lโ€™utilizzo di un interfaccia o di una classe astratta รจ obbligatorio.

6. Delegate per evitare lo switch case

In accordo con la antiifcampaign รจ una pratica di buona programmazione ridurre al minimo lโ€™utilizzo degli if basati sul confronto tra una variabile e n costanti, come nel seguente esempio:

if(input == "foo")
{
  Writeln("some logic here");
}
else if(input == "bar")
{
  Writeln("something else here");
}
else if(input == "raboof")
{
  Writeln("of course I need more than just Writeln");
}

Esistono due modalitร  di buona programmazione che eliminano questo caso:

  • il pattern strategy (che approfondirรฒ in un articolo successivo);
  • i delegate.

Analizziamo il secondo metodo: creiamo un dizionario che associa una stringa ad una funzione delegata

delegate void DoStuff();
IDictionary<string, DoStuff> dict = new Dictionary<string, DoStuff>();
dict["foo"] = delegate { Console.WriteLine("some logic here"); };
dict["bar"] = delegate { Console.WriteLine("something else here"); };
dict["raboof"] = delegate { Console.WriteLine("of course I need more than just Writeln"); };

Ogni funzione presente come valore nel dizionario indica una logica da implementare nellโ€™if.

Per richiamare la funzione corretta in base alla variabile input basta eseguire il seguente codice:

dict["foo"]();