Come usare gli oggetti Delegate in C#

moentecitorio

L'immagine qui sopra introduce l'argomento che mi accingo a trattare oggi, che ha destato particolare interesse durante la presentazione del collega e amico Pietro Brambati su LinQ che lui ha dedicato agli Student Partner ospitati nei nostri uffici di Segrate questa settimana. L'accostamento è forse un po' forzato, tuttavia il significato è che così come il Parlamento italiano è delegato ogni cinque anni dai cittadini a prendere delle decisioni, gli oggetti Delegate sono demandati a rappresentare funzioni. L'aspetto peculiare è che l'associazione fra delegate e funzioni non è disponibile a tempo di compilazione. Ciò rende estremamente potente e utile l'uso dei delegate in casi in cui, come negli esempi che seguono, si voglia loggare l'avanzamento di un programma senza sapere, a compile time, quali informazioni devono essere passate; un'altra applicazione sono i sistemi event-driven quando si vuole che una porzione di codice venga eseguita quando scatta un evento come un mouse click.

Chi ha già usato i puntatori a funzione in C++ starà sicuramente facendo un parallelo con questi, il che è abbastanza corretto; si tenga presente però che essendo i Delegate degli oggetti esposti dalla libreria di classi del framework .NET, per precisione si dovrebbe dire che essi incapsulano un riferimento ad (uno o più) metodi pubblici di una classe e vengono normalmente passati a codice che li richiama senza sapere, se non a run-time, quale metodo sarà effettivamente invocato.

Partiamo dal caso classico in cui si richiama, senza delegate, un metodo pubblico di una classe. A tal proposito potete creare un nuovo progetto di tipo console con il Visual Studio (nel mio caso l'ho chiamato Delegati) e aggiungere, dentro il namespace, la seguente classe:

class FunzioniUtili
{
 static public void Tracking(string localString) {Console.WriteLine(localString);}
}

Come si evince chiaramente, essa altro non fa che pubblicare un metodo Tracking che scrive sulla console la stringa che gli viene passata; questo metodo non restituisce alcun valore. Ho dichiarato il metodo come statico per evitare di dover istanziare un oggetto della classe prima di richiamarlo.

Creiamo ora un'altra classe nello stesso namespace, che useremo per testare il funzionamento delle diverse chiamate al metodo Tracking; il suo metodo TestMethod1()  richiama Tracking in modo diretto:

class TestClass
  {
    static public void TestMethod1()
      {FunzioniUtili.Tracking("chiamata diretta");}
  }

Ci siamo: richiamiamo TestMethod1 dal main della nostra applicazione con l'istruzione TestClass.TestMethod1(); -che anche in questo caso non richiede l'istanziazione della classe in quanto si tratta di metodo statico- e avremo la stringa "chiamata diretta" stampata sulla console. Se non lanciate l'applicazione da riga di comando, ricordatevi di mettere l'istruzione Console.ReadLine(); prima della chiusura del main, altrimenti non farete in tempo a vedere il risultato ;-).

 

Adesso andiamo invece a richiamare Tracking utilizzando un oggetto delegate che chiamiamo Delegato1, definito sempre nella TestClass appena prima di scrivere la nuova funzione TestMethod2. Tale oggetto, definito fuori, viene instanziato all'interno della funzione passando al suo costruttore il metodo da richiamare:

delegate void Delegato1(string strInput);
static public void TestMethod2()
{
    Delegato1 delegato1 = new Delegato1(FunzioniUtili.Tracking);
    delegato1("chiamata tramite delegate");
    //Le due righe sopra sono equivalenti a scrivere inline:
//new Delegato1(FunzioniUtili.Tracking)(("chiamata tramite delegate"));
}

Analogamente al test precedente, aggiungete l'istruzione TestClass.TestMethod2(); nel main subito dopo la riga TestClass.TestMethod1(); e verificate il risultato: anche la seconda stringa viene stampata nella console, ovvero il metodo Tracking viene correttamente richiamato attraverso il reference Delegato1.

 

Proviamo ora a salire di un altro livello di astrazione: vogliamo che il metodo Tracking venga richiamato dall'interno di una funzione che accetta come parametro un delegate; a questo scopo definiamo un altro delegate (che chiamiamo Delegato2) come oggetto pubblico della classe TestClass, in modo che lo si possa istanziare dall'esterno e poi passare l'oggetto come parametro al nuovo metodo TestMethod3:

public delegate void Delegato2(string strInput);
static public void TestMethod3(Delegato2 delegato2)
{ delegato2("chiamata tramite delegate passato come parametro"); }

Come dicevo, questa volta nel main dobbiamo mettere un'istruzione in più, per creare l'oggetto di tipo Delegato2; andate a capo dopo TestClass.TestMethod2(); e scrivete questo:

   TestClass.Delegato2 delegato2 = new TestClass.Delegato2(FunzioniUtili.Tracking);
   TestClass.TestMethod3(delegato2);
   //Le due righe sopra sono equivalenti a scrivere inline:
//TestClass.TestMethod3(new TestClass.Delegato2(FunzioniUtili.Tracking));

Ancora una volta eseguiamo il programma: anche la terza stringa sarà stampata nella console.

 

Salvate ora questo progetto (potete anche trovare i sorgenti qui) e creiamone un altro ex-novo (io l'ho chiamato Delegati2), in quanto altrimenti dovremmo modificare il precedente in modo troppo invasivo. Questa volta metteremo il metodo statico Tracking che scrive sulla console all'interno della classe principale dell'applicazione (dove si trova anche il Main) e lo richiameremo attraverso un delegate definito e utilizzato nella classe FunzioniUtili, ma istanziato e quindi passato come parametro dall'interno del Main. Vediamo un passo alla volta.

Per prima cosa scriviamo la funzione che stampa sulla console la stringa ricevuta; mettete la seguente porzione di codice subito dopo la chiusura dalla funzione Main dentro la classe Program:

//definizione del metodo che corrisponde alla signature
//dell'oggetto LogHandler definito in MyClass:
static void Tracking(string localString)
{ Console.WriteLine(localString); }

Successivamente, nello stesso namespace creiamo una nuova classe al cui interno dichiariamo -senza istanziare- un oggetto delegate pubblico ad un metodo che prende una stringa e non restituisce valori (che sarà poi quello che referenzia il metodo Tracking); subito dopo la definizione del delegate implementiamo una funzione che prende un oggetto di tipo delegate e lo esegue:

class MyClass
{
    //dichiarazione di un delegate che prende una stringa
//e non restituisce alcun valore:
    public delegate void LogHandler(string localString);
    public void RunDelegate(LogHandler loghandler)
    { loghandler("stringa da stampare in console"); }
}

A questo punto non rimane che creare un'istanza del delegate e passarlo come parametro al metodo RunDelegate; immettete questo codice nel Main:

MyClass.LogHandler loghandler = new MyClass.LogHandler(Tracking);
MyClass myclass = new MyClass();
myclass.RunDelegate(loghandler);
Console.ReadLine();

Finito: eseguite il programma e otterrete la stringa stampata in console attraverso l'esecuzione di un metodo statico implementato nella classe Program, a sua volta richiamato da un metodo (in questo caso: TestDelegate) di una classe separata (MyClass) che prende come parametro un delegate alla funzione da chiamare. Trovate il progetto compelto Delegati2 a questo indirizzo.

 

 

Volendo, si può fare di più. Infatti, in C# i delegate sono multicast, il che significa che possono puntare a più di una funzione alla volta. Un delegate multicast mantiene una lista di funzioni che saranno tutte chiamate quando viene invocato il delegate.

 

Concludo il post con un cenno alla programmazione ad eventi, che a ben vedere è molto legata all'argomento trattato in questo articolo. Il principio è quello dell'"Editore e Abbonato": in questo modello asincrono, gli editori implementano la business logic e pubblicano un "evento", inviando la notifica ai soli abbonati che lo hanno sottoscritto. In C#, qualunque oggetto può pubblicare un insieme di eventi che altre applicazioni possono sottoscrivere; la figura sotto mostra un esempio di questo meccanismo:

EventModel

RIFERIMENTI

MSDN LIBRARY: Introduzione ai delegate (in inglese): https://msdn.microsoft.com/msdnmag/issues/01/04/net/