Questo sito utilizza cookies, anche di terze parti, per mostrare pubblicità e servizi in linea con il tuo account. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - I Delegate

Guida al Visual Basic .NET

Capitolo 34° - I Delegate

<< Precedente Prossimo >>


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.


<< Precedente Prossimo >>
A proposito dell'autore

Programmatore e analista .NET 2005/2008/2010 (in particolare C# e VB.NET), anche nell'implementazione Mono per Linux. Conoscenze approfondite di Pascal, PHP, XML, HTML 4.01/5, CSS 2.1/3, Javascript (e jQuery). Conoscenze buone di C, LUA, GML, Ruby, XNA, AJAX e Assembly 68000. Competenze basilari di C++, SQL, Hlsl, Java.