|
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).
Mi rendo conto che questa introduzione può apparire un po' troppo teorica e fumosa, ma serve per comprendere il comportamento
dei delegate.
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 Module
La 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 Module
Nel 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.
|
|