Con il termine Delegate si indica un particolare tipo di dato che è in grado di "contenere" un metodo, ossia una procedura o una funzione. Ho messo di proposito le virgolette sul verbo "contenere", poiché non è propriamente esatto, ma serve per rendere più incisiva la definizione. Come esistono tipi di dato per gli interi, i decimali, le date, le stringhe, gli oggetti, ne esistono anche per i metodi, anche se può sembrare un po' strano. Per chi avesse studiato altri linguaggi prima di approcciarsi al VB.NET, possiamo assimilare i Delegate ai tipi procedurali del Pascal o ai puntatori a funzione del C. Ad ogni modo, i delegate sono leggermente diversi da questi ultimi e presentano alcuni tratti particolari:
- Un delegate non può contenere qualsiasi metodo, ma he dei limiti. Infatti, è in grado di contenere solo metodi con la stessa signature specificata nella definizione del tipo. Fra breve vedremo in cosa consiste questo punto;
- Un delegate può contenere sia metodi di istanza sia metodi statici, a patto che questi rispettino la regole di cui al punto sopra;
- Un delegate è un tipo reference, quindi si comporta come un comunissimo oggetto, seguendo quelle regole che mi sembra di aver già ripetuto fino alla noia;
- Un oggetto di tipo delegate è un oggetto immutabile, ossia, una volta creato, non può essere modificato. Per questo motivo, non espone alcuna proprietà (tranne due in sola lettura). D'altra parte, questo comportamento era prevedibile fin dalla definizione: infatti, se un delegate contiene un riferimento ad un metodo - e quindi un metodo già esistente e magari definito in un'altra parte del codice - come si farebbe a modificarlo? Non si potrebbe modificare la signature perchè questo andrebbe in conflitto con la sua natura, e non si potrebbe modificarne il corpo perchè si tratta di codice già scritto (ricordate che gli oggetti esistono solo a run-time, perchè vengono creati solo dopo l'avvio del programma, e tutto il codice è già stato compilato e trasformato in linguaggio macchina intermedio);
- Un delegate è un tipo safe, ossia non può mai contenere riferimenti ad indirizzi di memoria che non indichino espressamente un metodo (al contrario dei pericolosi puntatori del C).
Dichiarazione di un delegate
Un nuovo tipo delegate viene dichiarato con questa sintassi:Delegate [Sub/Function] [Nome]([Elenco parametri])Appare subito chiaro il legame con i metodi data la fortissima somiglianza della sintassi con quella usata per definire, appunto, un metodo. Notate che in questo caso si specifica solo la signature (tipo e quantità dei parametri) e la categoria (procedura o funzione) del delegate, mentre il [Nome] indica il nome del nuovo tipo creato (così come il nome di una nuova classe o una nuova struttura), ma non vi è traccia del "corpo" del delegate. Un delegate, infatti, non ha corpo, perchè, se invocato da un oggetto, esegue i metodi che esso stesso contiene, e quindi esegue il codice contenuto nei loro corpi. Da questo momento in poi, potremo usare nel codice questo nuovo tipo per immagazzinare interi metodi con le stesse caratteristiche appena definite. Dato che si tratta di un tipo reference, però, bisogna anche inizializzare l'oggetto con un costruttore... Qui dovrebbe sorgere spontaneamente un dubbio: dove e come si dichiara il costruttore di un delegate? Fino ad ora, infatti, gli unici tipi reference che abbiamo imparato a dichiarare sono le classi, e nelle classi è lecito scrivere un nuovo costruttore New nel loro corpo. Qui, invece, non c'è nessun corpo in cui porre un ipotetico costruttore. La realtà è che si usa sempre il costruttore di default, ossia quello predefinito, che viene automaticamente creato all'atto stesso della dichiarazione, anche se noi non riusciamo a vederlo. Questo costruttore accetta sempre e solo un parametro: un oggetto di tipo indeterminato restituito da uno speciale operatore, AddressOf. Questo è un operatore unario che accetta come operando il metodo di cui ottenere l'"indirizzo":
AddressOf [NomeMetodo]Ciò che AddressOf restituisce non è molto chiaro: la sua descrizione dice espressamente che viene restituito un oggetto delegate (il che è già abbastanza strano di per sé, dato che per creare un delegate ci vuole un altro delegate). Tuttavia, se si utilizza come parametro del costruttore un oggetto System.Delegate viene restituito un errore. Ma lasciamo queste disquisizioni a chi ha tempo da perdere e procediamo con le cose importanti.
N.B.: Dalla versione 2008, i costruttori degli oggetti delegate accettano anche espressioni lambda!
Una volta dichiarata ed inizializzata una variabile di tipo delegate, è possibile usarla esattamente come se fosse un metodo con la signature specificata. Ecco un esempio:
Module Module1 'Dichiarazione di un tipo delegate Sub che accetta un parametro 'di tipo stringa. Delegate Sub Display(ByVal Message As String) 'Una procedura dimostrativa Sub Write1(ByVal S As String) Console.WriteLine("1: " & S) End Sub 'Un'altra procedura dimostrativa Sub Write2(ByVal S As String) Console.WriteLine("2: " & S) End Sub Sub Main() 'Variabile D di tipo Display, ossia il nuovo tipo 'delegate appena definito all'inizio del modulo Dim D As Display 'Inizializa D con un nuovo oggetto delegate contenente 'un riferimento al metodo Console.WriteLine D = New Display(AddressOf Console.WriteLine) 'Invoca il metodo referenziato da D: in questo caso 'equivarrebbe a scrivere Console.WriteLine("Ciao") D("Ciao") 'Reinizializza D, assegnandogli l'indirizzo di Write1 D = New Display(AddressOf Write1) 'è come chiamare Write1("Ciao") D("Ciao") 'Modo alternativo per inizializzare un delegate: si omette 'New e si usa solo AddressOf. Questo genera una conversione 'implicita che dà errore di cast nel caso in cui Write1 'non sia compatibile con la signature del delegate D = AddressOf Write2 D("Ciao") 'Notare che D può contenere metodi di istanza '(come Console.WriteLine) e metodi statici (come Write1 'e Write2) Console.ReadKey() End Sub End ModuleLa signature di un delegate non può contenere parametri indefiniti (ParamArray) od opzionali (Optional), tuttavia i metodi memorizzati in un oggetto di tipo delegate possono avere parametri di questo tipo. Eccone un esempio:
Module Module1 'Tipo delegate che può contenere riferimenti a funzioni Single 'che accettino un parametro di tipo array di Single Delegate Function ProcessData(ByVal Data() As Single) As Single 'Tipo delegate che può contenere riferimenti a procedure 'che accettino due parametri, un array di Single e un Boolean Delegate Sub PrintData(ByVal Data() As Single, ByVal ReverseOrder As Boolean) 'Funzione che calcola la media di alcuni valori. Notare che 'l'unico parametro è indefinito, in quanto 'dichiarato come ParamArray Function CalculateAverage(ByVal ParamArray Data() As Single) As Single Dim Total As Single = 0 For I As Int32 = 0 To Data.Length - 1 Total += Data(I) Next Return (Total / Data.Length) End Function 'Funzione che calcola la varianza di alcuni valori. Notare che 'anche in questo caso il parametro è indefinito Function CalculateVariance(ByVal ParamArray Data() As Single) As Single Dim Average As Single = CalculateAverage(Data) Dim Result As Single = 0 For I As Int32 = 0 To Data.Length - 1 Result += (Data(I) - Average) ^ 2 Next Return (Result / Data.Length) End Function 'Procedura che stampa i valori di un array in ordine normale 'o inverso. Notare che il secondo parametro è opzionale Sub PrintNormal(ByVal Data() As Single, _ Optional ByVal ReverseOrder As Boolean = False) If ReverseOrder Then For I As Int32 = Data.Length - 1 To 0 Step -1 Console.WriteLine(Data(I)) Next Else For I As Int32 = 0 To Data.Length - 1 Console.WriteLine(Data(I)) Next End If End Sub 'Procedura che stampa i valori di un array nella forma: '"I+1) Data(I)" 'Notare che anche in questo caso il secondo parametro 'è opzionale Sub PrintIndexed(ByVal Data() As Single, _ Optional ByVal ReverseOrder As Boolean = False) If ReverseOrder Then For I As Int32 = Data.Length - 1 To 0 Step -1 Console.WriteLine("{0}) {1}", Data.Length - I, Data(I)) Next Else For I As Int32 = 0 To Data.Length - 1 Console.WriteLine("{0}) {1}", (I + 1), Data(I)) Next End If End Sub Sub Main() Dim Process As ProcessData Dim Print As PrintData Dim Data() As Single Dim Len As Int32 Dim Cmd As Char Console.WriteLine("Quanti valori inserire?") Len = Console.ReadLine ReDim Data(Len - 1) For I As Int32 = 1 To Len Console.Write("Inserire il valore " & I & ": ") Data(I - 1) = Console.ReadLine Next Console.Clear() Console.WriteLine("Scegliere l'operazione da eseguire: ") Console.WriteLine("m - Calcola la media dei valori;") Console.WriteLine("v - Calcola la varianza dei valori;") Cmd = Console.ReadKey().KeyChar Select Case Cmd Case "m" Process = New ProcessData(AddressOf CalculateAverage) Case "v" Process = New ProcessData(AddressOf CalculateVariance) Case Else Console.WriteLine("Comando non valido!") Exit Sub End Select Console.WriteLine() Console.WriteLine("Scegliere il metodo di stampa: ") Console.WriteLine("s - Stampa i valori;") Console.WriteLine("i - Stampa i valori con il numero ordinale a fianco.") Cmd = Console.ReadKey().KeyChar Select Case Cmd Case "s" Print = New PrintData(AddressOf PrintNormal) Case "i" Print = New PrintData(AddressOf PrintIndexed) Case Else Console.WriteLine("Comando non valido!") Exit Sub End Select Console.Clear() Console.WriteLine("Valori:") 'Eccoci arrivati al punto. Come detto prima, i delegate 'non possono definire una signature che comprenda parametri 'opzionali o indefiniti, ma si 'può aggirare questa limitazione semplicemente dichiarando 'un array di valori al posto del ParamArray (in quanto si 'tratta comunque di due vettori) e lo stesso parametro 'non opzionale al posto del parametro opzionale. 'L'inconveniente, in questo ultimo caso, è che il 'parametro, pur essendo opzionale va sempre specificato 'quando il metodo viene richiamato attraverso un oggetto 'delegate. Questo escamotage permette di aumentare la 'portata dei delegate, includendo anche metodi che 'possono essere stati scritti tempo prima in un'altra 'parte inaccessibile del codice: così 'non è necessario riscriverli! Print(Data, False) Console.WriteLine("Risultato:") Console.WriteLine(Process(Data)) Console.ReadKey() End Sub End Module
Un esempio più significativo
I delegate sono particolarmente utili per risparmiare spazio nel codice. Tramite i delegate, infatti, possiamo usare lo stesso metodo per eseguire più compiti differenti. Dato che una variabile delegate contiene un rifriento ad un metodo qualsiasi, semplicemente cambiando questo riferimento possiamo eseguire codici diversi richiamando la stessa variabile. E' come se potessimo "innestare" del codice sempre diverso su un substrato costante. Ecco un esempio piccolo, ma significativo:Module Module2 'Nome del file da cercare Dim File As String 'Questo delegate referenzia una funzione che accetta un 'parametro stringa e restituisce un valore booleano Delegate Function IsMyFile(ByVal FileName As String) As Boolean 'Funzione 1, stampa il contenuto del file a schermo Function PrintFile(ByVal FileName As String) As Boolean 'Io.Path.GetFileName(F) restituisce solo il nome del 'singolo file F, togliendo il percorso delle cartelle If IO.Path.GetFileName(FileName) = File Then 'IO.File.ReadAllText(F) restituisce il testo contenuto 'nel file F in una sola operazione Console.WriteLine(IO.File.ReadAllText(FileName)) Return True End If Return False End Function 'Funzione 2, copia il file sul desktop Function CopyFile(ByVal FileName As String) As Boolean If IO.Path.GetFileName(FileName) = File Then 'IO.File.Copy(S, D) copia il file S nel file D: 'se D non esiste viene creato, se esiste viene 'sovrascritto IO.File.Copy(FileName, _ My.Computer.FileSystem.SpecialDirectories.Desktop & _ "" & File) Return True End If Return False End Function 'Procedura ricorsiva che cerca il file Function SearchFile(ByVal Dir As String, ByVal IsOK As IsMyFile) _ As Boolean 'Ottiene tutte le sottodirectory Dim Dirs() As String = IO.Directory.GetDirectories(Dir) 'Ottiene tutti i files Dim Files() As String = IO.Directory.GetFiles(Dir) 'Analizza ogni file per vedere se è quello cercato For Each F As String In Files 'È il file cercato, basta cercare If IsOK(F) Then 'Termina la funzione e restituisce Vero, cosicché 'anche nel for sulle cartelle si termini 'la ricerca Return True End If Next 'Analizza tutte le sottocartelle For Each D As String In Dirs If SearchFile(D, IsOK) Then 'Termina ricorsivamente la ricerca Return True End If Next End Function Sub Main() Dim Dir As String Console.WriteLine("Inserire il nome file da cercare:") File = Console.ReadLine Console.WriteLine("Inserire la cartella in cui cercare:") Dir = Console.ReadLine 'Cerca il file e lo scrive a schermo SearchFile(Dir, AddressOf PrintFile) 'Cerca il file e lo copia sul desktop SearchFile(Dir, AddressOf CopyFile) Console.ReadKey() End Sub End ModuleNel sorgente si vede che si usano pochissime righe per far compiere due operazioni molto differenti alla stessa procedura. In altre condizioni, un aspirante programmatore che non conoscesse i delegate avrebbe scritto due procedure intere, sprecando più spazio, e condannandosi, inoltre, a riscrivere la stessa cosa per ogni futura variante.
A cura di: Il Totem