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