Delegates

Basics

Una delle cose che trovo più importanti, ma al tempo stesso ostiche dal punto di vista della comprensione logica di ciò che si sta facendo, è l'uso dei delegate, e di conseguenza degli eventi.

Siamo abituati a utilizzare sempre metodi per l'esecuzione di determinati algoritmi, particolarità dei metodi:

  1. Accettano parametri in ingresso

  2. Hanno un return type specifico

Un buon modo per descrivere cosa è un delegate, per chi non avesse un po' di "memoria storica" su function pointer e callback functions, è di pensare ad un delegate come a qualcosa che dà un nome ad un metodo e ne descrive la signature per la chiamata a tale metodo.

Supponiamo adesso di voler definire quindi un delegate, poi vedremo come usarlo, per un metodo che chiameremo GetString e restituisca una stringa.

public delegate string GetString();

Abbiamo quindi appena definito il nostro delegate, a questo delegate possiamo associare quindi qualsiasi metodo che abbia la stessa signature e restituisca una stringa, certo che per dare significato a tutto questo abbiamo bisogno di un metodo che effettivamente mi ritorni quello che mi interessa.

vediamo un esempio:

   1: public delegate string GetString(); 
   2: public static void Main(String[] args) { 
   3: int x = 20; 
   4: GetIntString delFunction = new GetIntString(x.ToString); 
   5:     Console.WriteLine("Il risultato dell'invocazione del delegate restituisce semplicemente il metodo ToString() applicato all'int: {0}",delFunction()); 
   6: } 

La chiamata a delFunction() equivale sintatticamente alla chiamata delFunction.Invoke().
Facciamo ora un passo in avanti, e consideriamo un paio di punti importanti per i delegate:

  1. Non è importante il tipo su cui si invoca il metodo, come già detto l'importante è uguale signature e uguale return type.

  2. Tutto funziona anche con l'uso di metodi statici passati come target del delegate.

Creiamo una nuova classe Person per dimostrare questi due punti:

   1: public class Person { 
   2:     public string Nome {get; set;} 
   3:     public string Cognome {get;set;} 
   4:     public string ToString() 
   5:     { 
   6:         return String.Format("{0}, {1}", Cognome, Nome); 
   7:     } 
   8:     public static string StaticGetString() 
   9:     { 
  10:         return "static string"; 
  11:     } 
  12: } 

e modifichiamo il nostro metodo Main ...

   1: public static void Main(String[] args) { 
   2: Person p = new Person() {Nome="Gianluca", Cognome="Gravina"}; 
   3: delFunction = new GetString(p.ToString); 
   4:     Console.WriteLine(delFunction()); 
   5: delFunction = new GetString(Person.StaticGetString); 
   6:     Console.WriteLine(delFunction()); 
   7: } 

Possiamo notare quindi che non importa l'oggetto su cui viene chiamato il delegate, né se il metodo è statico o meno.
In .Net2.0 sono state introdotte alcune migliorie sintattiche per l'uso dei delegate.
E' possibile scrivere:

   1: delFunction = p.ToString; 

al posto di

   1: delFunction = new GetString(p.ToString); 

è inoltre possibile usare metodi anonimi:

   1: ... 
   2: string content = " - content - "; 
   3: delFunction = delegate(string data) { 
   4:     data += content; 
   5:     data += "post"; 
   6:     return data; 
   7: }; 
   8: delFunction("pre"); 
   9: ... 

Stamperà a video: "pre - content - post".
Vediamo ora un esempio pratico di uso dei delegate con il classico algoritmo di sort BubbleSort
creiamo la classe BubbleSorter con un metodo statico per il sort:

   1: public class BubbleSorter 
   2: { 
   3:     static public void Sort(object[] sortArray, CompareOp cmpMethod) 
   4:     { 
   5:         for (int i=0 ; i<sortArray.Length ; i++) 
   6:         { 
   7:             for (int j=i+1 ; j<sortArray.Length ; j++) 
   8:             { 
   9:                 if (cmpMethod(sortArray[j], sortArray[i])) 
  10:                 { 
  11:                     object temp = sortArray[i]; 
  12:                     sortArray[i] = sortArray[j]; 
  13:                     sortArray[j] = temp; 
  14:                 } 
  15:             } 
  16:         } 
  17:     } 
  18: } 

creiamo il nostro delegate:

   1: public delegate bool CompareOp(object l, object r); 

e nel nostro Main:

   1: ... 
   2: object[] array = new object[] {3,1,5,2,9,6}; 
   3: CompareOp greater = delegate(object l, object r) { 
   4:     return (int)l > (int)r ? true : false; 
   5: } 
   6: BubbleSorter.Sort(array, greater); 

Il risultato sarà il nostro array ordinato in ordine decrescente.
Cosa molto comoda, nel momento in cui volessimo ordinare in ordine decrescente, possiamo semplicemente creare un nuovo anonymous delegate come quello qui sotto:

   1: CompareOp lower = delegate(object l, object r) { 
   2:     return (int)l < (int)r ? true : false; 
   3: } 

e cambiare la chiamata al metodo Sort di BubbleSorter:

   1: BubbleSorter.Sort(array, lower); 

MultiCast Delegates

Fino ad ora abbiamo usato i Delegates solo per "eseguire" metodi, ma come facciamo a passare ad un delegate più di un metodo ? I multicast delegate, che poi vedremo essere strettamente legati agli eventi, permettono di farlo. La definizione di un MultiCast Delegate è assolutamente identica a quella di un delegate normalissimo, l'unica accortezza è che il return type deve essere un void.
Vediamo un esempio:

   1: delegate void MathMulticast(int x); 
   2:  
   3: public class MathOperations { 
   4:     public static void DoubleMe(int x) 
   5:     { 
   6:         int result = x * 2; 
   7:         Console.WriteLine(result); 
   8:     } 
   9:     public static void PowerMe(int x) 
  10:     { 
  11:         int result = x * x; 
  12:         Console.WriteLine(result); 
  13:     } 
  14: } 
  15:  
  16: public class Program { 
  17:     public static void Main(String[] args) 
  18:     {    
  19:         MathMulticast operations = new MathMulticast(MathOperations.DoubleMe); 
  20:         operations += MathOperations.PowerMe; 
  21:         ProcessAndDisplay(operations, 2); 
  22:         ProcessAndDisplay(operations, 12); 
  23:         Console.WriteLine(); 
  24:     } 
  25:     
  26:     public void ProcessAndDisplay ( MathMulticast action, int value) 
  27:     { 
  28:         action(value); 
  29:     } 
  30: } 

Nell'esempio qui sopra descritto abbiamo visto come il multicast delegate accetta, per aggiungere metodi al delegate, sia l'operatore + (operations = operations + ...) sia l'operatore += (operations += ...).
Il risultato è che per ogni valore passato al metodo PorcessAndDisplay vengono lanciati tutti i metodi del MultiCast delegate. Da notare:

  1. L'ordine dell'invocazione dei metodi è non prevedibile, è quindi sconsigliata la scrittura di metodi che dipendano dall'ordine del lancio dei metodi.
  2. Se viene lanciata una eccezione durante la chiamata di uno di questi metodi, gli altri non vengono eseguiti.

Esiste comunque un metodo per evitare il punto 2 ed è quello di ciclare a mano sui metodi presenti nel multicast delegate:

   1: public void ProcessAndDisplay (MathMulticast action, int value) 
   2: { 
   3:     Delegate[] delegates = action.GetInvocationList(); 
   4:     foreach (Delegate d in delegates) 
   5:     { 
   6:         try { 
   7:             d.DynamicInvoke(value); 
   8:         } catch (Exception exc) { 
   9:             Console.WriteLine("Exception thrown"); 
  10:         } 
  11:     } 
  12: } 

In questo modo, nel caso in uno dei metodi eseguiti dal delegate, nel caso di un'eccezione lanciata, viene gestita e soprattutto gli altri metodi registrati sul delegate continueranno il loro ciclo.

Delegate Asincroni

I delegate seguono il paradigma di programmazione asincrono APM. Esistono quindi dei metodi BeginInvoke e EndInvoke, il primo ritorna un IAsyncResult, il secondo si aspetta un IAsyncResult come parametro di ingresso.
Esistono diversi modi per invocare metodi asincroni con i delegate vediamoli nel dettaglio.
Polling

Dichiariamo subito un delegate che ha bisogno di un po' di tempo per finire la propria esecuzione.
public delegate int TakesAWhile(int data, int ms);
E scriviamo una implementazione di metodo che vogliamo assegnare a questo delegate:

   1: public static int LongOperation(int data, int ms) 
   2: { 
   3:     Console.WriteLine("Starting..."); 
   4:     Thread.Sleep(ms); 
   5:     Console.WriteLine("Ending..."); 
   6:     return ++data; 
   7: } 

Abbiamo quindi il nostro delegate e il metodo che intendiamo invocare in maniera asincrona tramite il delegate. Scriviamo ora il codice per effettuare una chiamata asincrona e gestirne il risultato.

   1: public static void Main(String[] args) 
   2: { 
   3:     TakesAWhile longDelegate = LongOperation; 
   4:     IAsyncResult ar = longDelegate.BeginInvoke(0,3000,null,null); 
   5:     while (ar.IsCompleted) 
   6:     { 
   7:         Console.WriteLine("."); 
   8:         Thread.Sleep(50); 
   9:     } 
  10:     int result = longDelegate.EndInvoke(ar); 
  11:     Console.WriteLine(result.ToString()); 
  12: } 

Abbiamo quindi gestito il fatto che un nuovo thread è partito (quello invocato dal delegate) e il thread chiamante rimane attivo e in attesa della fine del thread asincorno appena lanciato.

WaitHandle

Analogamente a quanto visto in precedenza, in cui il chiamante effettua un polling ogni "tot" tempo per controllare se il chiamato ha completato il suo "lavoro", vediamo ora una soluzione alternativa in cui è il chiamato che avvisa il chiamante del lavoro finito utilizzando una notify su Thread messo a disposizione del delegate.

   1: public static void Main(String[] args) 
   2: { 
   3:     TakesAwhile longDelegate = LongOperation; 
   4:     IAsyncResult ar = longDelegate.BeginInvoke(0,3000,null,null); 
   5:     while (true) 
   6:     { 
   7:         if (ar.AsynWaitHandle.WaitOne()) 
   8:         { 
   9:             Console.WriteLine("Job Finished"); 
  10:             break; 
  11:         } 
  12:     } 
  13:     int result = longDelegate.EndInvoke(ar); 
  14:     Console.WriteLine(result.ToString()); 
  15: } 


Async Callback

Un ultimo metodo per l'utilizzo di metodi asincroni mediante delegate, vediamo gli async callback. Si tratta sostanzialmente dello stesso codice dei WaitHandle, ma invece di rimanere in attesa del risultato su un Thread (WaitHandle), il chiamato chiama una funzione di callback sul chiamante al termine dell'esecuzione del codice del delegate.

   1: public static void Main(String[] args) 
   2: { 
   3:     TakesAWhile longDelegate = LongOperation; 
   4:     IAsyncResult ar = longDelegate.BeginInvoke(0,3000,callBack,longDelegate); 
   5:     while (!ar.IsCompleted) 
   6:     { 
   7:         Thread.Sleep(50); 
   8:     } 
   9: } 
  10:  
  11: public void callBack(IAsyncResult ar) 
  12: { 
  13:     if (ar == null) Throw new ArgumentNullException("ar"); 
  14:     TakesAWhile longDelegate = ar.AsyncState as TakesAWhile; 
  15:     int result = longDelegate.EndInvoke(ar); 
  16:     Console.WriteLine(result.ToString()); 
  17: } 

Con questa soluzione, il chiamante si "dimentica" assolutamente del metodo chiamato in maniera asincrona, è il chiamato che notifica il chiamante. Una piccola nota, il ciclo alla fine del metodo Main è stato messo perché altrimenti, se il processo finisce, non viene notificata la fine del metodo.