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"]();