Al contrario di un delegate semplice, un delegate multicast può contenere riferimenti a più metodi insieme, purché della stessa categoria e con la stessa signature. Dato che il costruttore è sempre lo stesso e accetta un solo parametro, non è possibile creare delegate multicast in fase di inizializzazione. L'unico modo per farlo è richiamare il metodo statico Combine della classe System.Delegate (ossia la classe base di tutti i delegate). Combine espone anche un overload che permette di unire molti delegate alla volta, specificandoli tramite un ParamArray. Dato che un delegate multicast contiene più riferimenti a metodi distinti, si parla di invocation list (lista di invocazione) quando ci si riferisce all'insieme di tutti i metodi memorizzati in un delegate multicast. Ecco un semplice esempio:
Module Module2 'Vedi esempio precedente Sub Main() Dim Dir As String Dim D As IsMyFile Console.WriteLine("Inserire il nome file da cercare:") File = Console.ReadLine Console.WriteLine("Inserire la cartella in cui cercare:") Dir = Console.ReadLine 'Crea un delegate multicast, unendo PrintFile e CopyFile. 'Da notare che in questa espressione è necessario usare 'delle vere e proprie variabili delegate, poiché 'l'operatore AddressOf da solo non è valido in questo caso D = System.Delegate.Combine(New IsMyFile(AddressOf PrintFile), _ New IsMyFile(AddressOf CopyFile)) 'Per la cronaca, Combine è un metodo factory 'Ora il file trovato viene sia visualizzato che copiato 'sul desktop SearchFile(Dir, D) 'Se si vuole rimuovere uno o più riferimenti a metodi del 'delegate multicast si deve utilizzare il metodo statico Remove: D = System.Delegate.Remove(D, New IsMyFile(AddressOf CopyFile)) 'Ora D farà visualizzare solamente il file trovato Console.ReadKey() End Sub End ModuleLa funzione Combine, tuttavia, nasconde molte insidie. Infatti, essendo un metodo factory della classe System.Delegate, come abbiamo detto nel capitolo relativo ai metodi factory, restituisce un oggetto di tipo System.Delegate. Nell'esempio, noi abbiamo potuto assegnare il valore restituito da Combine a D, che è di tipo IsMyFile, perchè solitamente le opzioni di compilazione permettono di eseguire conversioni implicite di questo tipo - ossia Option Strict è solitamente impostato su Off (per ulteriori informazioni, vedere il capitolo sulle opzioni di compilazione). Come abbiamo detto nel capitolo sulle conversioni, assegnare il valore di una classe derivata a una classe base è lecito, poichè nel passaggio da una all'altra non si perde alcun dato, ma si generelizza soltanto il valore rappresentato; eseguire il passaggio inverso, invece, ossia assegnare una classe base a una derivata, può risultare in qualche strano errore perchè i membri in più della classe derivata sono vuoti. Nel caso dei delegate, che sono oggetti immutabili, e che quindi non espongono proprietà modificabili, questo non è un problema, ma il compilatore questo non lo sa. Per essere sicuri, è meglio utilizzare un operatore di cast come DirectCast:
DirectCast(System.Delegate.Combine(A, B), IsMyFile)N.B.: Quando un delegate multicast contiene delle funzioni e viene richiamato, il valore restituito è quello della prima funzione memorizzata.
Ecco ora un altro esempio molto articolato sui delegate multicast:
'Questo esempio si basa completamente sulla manipolazione 'di file e cartelle, argomento non ancora affrontato. Se volete, 'potete dare uno sguardo ai capitoli relativi nelle parti 'successive della guida, oppure potete anche limitarvi a leggere 'i commenti, che spiegano tutto ciò che accade. Module Module1 'In questo esempio eseguiremo delle operazioni su file con i delegate. 'Nel menù sarà possibile scegliere quali operazioni 'eseguire (una o tutte insieme) e sotto quali condizioni modificare 'un file. 'Il delegate FileFilter rappresenta una funzione che restituisce 'True se la condizione è soddisfatta. Le condizioni 'sono racchiuse in un delegate multicast che contiene più 'funzioni di questo tipo Delegate Function FileFilter(ByVal FileName As String) As Boolean 'Il prossimo delegate rappresenta un'operazione su un file Delegate Sub MassFileOperation(ByVal FileName As String) 'AskForData è un delegate del tipo più semplice. 'Servirà per reperire le informazioni necessarie ad 'eseguire le operazioni (ad esempio, se si sceglie di copiare 'tutti i file di una cartella, si dovrà anche scegliere 'dove copiare questi file). Delegate Sub AskForData() 'Queste variabili globali rappresentano le informazioni necesarie 'per lo svolgimento delle operazioni o la verifica delle condizioni. 'Stringa di formato per rinominare i file Dim RenameFormat As String 'Posizione di un file nella cartella Dim FileIndex As Int32 'Directory in cui copiare i file Dim CopyDirectory As String 'File in cui scrivere. Il tipo StreamWriter permette di scrivere 'facilmente stringhe su un file usando WriteLine come in Console Dim LogFile As IO.StreamWriter 'Limitazioni sulla data di creazione del file Dim CreationDateFrom, CreationDateTo As Date 'Limitazioni sulla data di ultimo accesso al file Dim LastAccessDateFrom, LastAccessDateTo As Date 'Limitazioni sulla dimensione Dim SizeFrom, SizeTo As Int64 'Rinomina un file Sub Rename(ByVal Path As String) 'Ne prende il nome semplice, senza estensione Dim Name As String = IO.Path.GetFileNameWithoutExtension(Path) 'Apre un oggetto contenente le informazioni sul file 'di percorso Path Dim Info As New IO.FileInfo(Path) 'Formatta il nome secondo la stringa di formato RenameFormat Name = String.Format(RenameFormat, _ Name, FileIndex, Info.Length, Info.LastAccessTime, Info.CreationTime) 'E aggiunge ancora l'estensione al nome modificato Name &= IO.Path.GetExtension(Path) 'Copia il vecchio file nella stessa cartella, ma con il nuovo nome IO.File.Copy(Path, IO.Path.GetDirectoryName(Path) & "" & Name) 'Elimina il vecchio file IO.File.Delete(Path) 'Aumenta l'indice di uno FileIndex += 1 End Sub 'Funzione che richiede i dati necessari per far funzionare 'il metodo Rename Sub InputRenameFormat() Console.WriteLine("Immettere una stringa di formato valida per rinominare i file.") Console.WriteLine("I parametri sono:") Console.WriteLine("0 = Nome originale del file;") Console.WriteLine("1 = Posizione del file nella cartella, in base 0;") Console.WriteLine("2 = Dimensione del file, in bytes;") Console.WriteLine("3 = Data dell'ultimo accesso;") Console.WriteLine("4 = Data di creazione.") RenameFormat = Console.ReadLine End Sub 'Elimina un file di percorso Path Sub Delete(ByVal Path As String) IO.File.Delete(Path) End Sub 'Copia il file da Path alla nuova cartella Sub Copy(ByVal Path As String) IO.File.Copy(Path, CopyDirectory & "" & IO.Path.GetFileName(Path)) End Sub 'Richiede una cartella valida in cui copiare i file. Se non esiste, la crea Sub InputCopyDirectory() Console.WriteLine("Inserire una cartella valida in cui copiare i file:") CopyDirectory = Console.ReadLine If Not IO.Directory.Exists(CopyDirectory) Then IO.Directory.CreateDirectory(CopyDirectory) End If End Sub 'Scrive il nome del file sul file aperto Sub Archive(ByVal Path As String) LogFile.WriteLine(IO.Path.GetFileName(Path)) End Sub 'Chiede il nome di un file su cui scrivere tutte le informazioni Sub InputLogFile() Console.WriteLine("Inserire il percorso del file su cui scrivere:") LogFile = New IO.StreamWriter(Console.ReadLine) End Sub 'Verifica che la data di creazione del file cada tra i limiti fissati Function IsCreationDateValid(ByVal Path As String) As Boolean Dim Info As New IO.FileInfo(Path) Return (Info.CreationTime >= CreationDateFrom) And (Info.CreationTime >= CreationDateTo) End Function 'Richiede di immettere una limitazione temporale per considerare 'solo certi file Sub InputCreationDates() Console.WriteLine("Verranno considerati solo i file con data di creazione:") Console.Write("Da: ") CreationDateFrom = Date.Parse(Console.ReadLine) Console.Write("A: ") CreationDateTo = Date.Parse(Console.ReadLine) End Sub 'Verifica che la data di ultimo accesso al file cada tra i limiti fissati Function IsLastAccessDateValid(ByVal Path As String) As Boolean Dim Info As New IO.FileInfo(Path) Return (Info.LastAccessTime >= LastAccessDateFrom) And (Info.LastAccessTime >= LastAccessDateTo) End Function 'Richiede di immettere una limitazione temporale per considerare 'solo certi file Sub InputLastAccessDates() Console.WriteLine("Verranno considerati solo i file con data di creazione:") Console.Write("Da: ") LastAccessDateFrom = Date.Parse(Console.ReadLine) Console.Write("A: ") LastAccessDateTo = Date.Parse(Console.ReadLine) End Sub 'Verifica che la dimensione del file sia coerente coi limiti fissati Function IsSizeValid(ByVal Path As String) As Boolean Dim Info As New IO.FileInfo(Path) Return (Info.Length >= SizeFrom) And (Info.Length >= SizeTo) End Function 'Richiede di specificare dei limiti dimensionali per i file Sub InputSizeLimit() Console.WriteLine("Verranno considerati solo i file con dimensione compresa:") Console.Write("Tra (bytes):") SizeFrom = Console.ReadLine Console.Write("E (bytes):") SizeTo = Console.ReadLine End Sub 'Classe che rappresenta un'operazione eseguibile su file Class Operation Private _Description As String Private _Execute As MassFileOperation Private _RequireData As AskForData Private _Enabled As Boolean 'Descrizione Public Property Description() As String Get Return _Description End Get Set(ByVal value As String) _Description = value End Set End Property 'Variabile che contiene l'oggetto delegate associato 'a questa operazione, ossia un riferimento a una delle Sub 'definite poco sopra Public Property Execute() As MassFileOperation Get Return _Execute End Get Set(ByVal value As MassFileOperation) _Execute = value End Set End Property 'Variabile che contiene l'oggetto delegate che serve 'per reperire informazioni necessarie ad eseguire 'l'operazione, ossia un riferimento a una delle sub 'di Input definite poco sopra. E' Nothing quando 'non serve nessun dato ausiliario (come nel caso 'di Delete) Public Property RequireData() As AskForData Get Return _RequireData End Get Set(ByVal value As AskForData) _RequireData = value End Set End Property 'Determina se l'operazione va eseguita oppure no Public Property Enabled() As Boolean Get Return _Enabled End Get Set(ByVal value As Boolean) _Enabled = value End Set End Property Sub New(ByVal Description As String, _ ByVal ExecuteMethod As MassFileOperation, _ ByVal RequireDataMethod As AskForData) Me.Description = Description Me.Execute = ExecuteMethod Me.RequireData = RequireDataMethod Me.Enabled = False End Sub End Class 'Classe che rappresenta una condizione a cui sottoporre 'i file nella cartella: verranno elaborati solo quelli che 'soddisfano tutte le condizioni Class Condition Private _Description As String Private _Verify As FileFilter Private _RequireData As AskForData Private _Enabled As Boolean Public Property Description() As String Get Return _Description End Get Set(ByVal value As String) _Description = value End Set End Property 'Contiene un oggetto delegate associato a una delle 'precedenti funzioni Public Property Verify() As FileFilter Get Return _Verify End Get Set(ByVal value As FileFilter) _Verify = value End Set End Property Public Property RequireData() As AskForData Get Return _RequireData End Get Set(ByVal value As AskForData) _RequireData = value End Set End Property Public Property Enabled() As Boolean Get Return _Enabled End Get Set(ByVal value As Boolean) _Enabled = value End Set End Property Sub New(ByVal Description As String, _ ByVal VerifyMethod As FileFilter, _ ByVal RequireDataMethod As AskForData) Me.Description = Description Me.Verify = VerifyMethod Me.RequireData = RequireDataMethod End Sub End Class Sub Main() 'Contiene tutte le operazioni da eseguire: sarà, quindi, un 'delegate multicast Dim DoOperations As MassFileOperation 'Contiene tutte le condizioni da verificare Dim VerifyConditions As FileFilter 'Indica la cartella di cui analizzare i file Dim Folder As String 'Hashtable di caratteri-Operation o carattri-Condition. Il 'carattere indica quale tasto è necessario 'premere per attivare/disattivare l'operazione/condizione Dim Operations As New Hashtable Dim Conditions As New Hashtable Dim Cmd As Char 'Aggiunge le operazioni esistenti. La 'c' messa dopo la stringa 'indica che la costante digitata è un carattere e non una 'stringa. Il sistema non riesce a distinguere tra stringhe di lunghezza 1 e caratteri, al contrario di come accade in C With Operations .Add("r"c, New Operation("Rinomina tutti i file nella cartella;", _ New MassFileOperation(AddressOf Rename), _ New AskForData(AddressOf InputRenameFormat))) .Add("c"c, New Operation("Copia tutti i file nella cartella in un'altra cartella;", _ New MassFileOperation(AddressOf Copy), _ New AskForData(AddressOf InputCopyDirectory))) .Add("a"c, New Operation("Scrive il nome di tutti i file nella cartella su un file;", _ New MassFileOperation(AddressOf Archive), _ New AskForData(AddressOf InputLogFile))) .Add("d"c, New Operation("Cancella tutti i file nella cartella;", _ New MassFileOperation(AddressOf Delete), _ Nothing)) End With 'Aggiunge le condizioni esistenti With Conditions .Add("r"c, New Condition("Seleziona i file da elaborare in base alla data di creazione;", _ New FileFilter(AddressOf IsCreationDateValid), _ New AskForData(AddressOf InputCreationDates))) .Add("l"c, New Condition("Seleziona i file da elaborare in base all'ultimo accesso;", _ New FileFilter(AddressOf IsLastAccessDateValid), _ New AskForData(AddressOf InputLastAccessDates))) .Add("s"c, New Condition("Seleziona i file da elaborare in base alla dimensione;", _ New FileFilter(AddressOf IsSizeValid), _ New AskForData(AddressOf InputSizeLimit))) End With Console.WriteLine("Modifica in massa di file ---") Console.WriteLine() Do Console.WriteLine("Immetti il percorso della cartella su cui operare:") Folder = Console.ReadLine Loop Until IO.Directory.Exists(Folder) Do Console.Clear() Console.WriteLine("Premere la lettera corrispondente per selezionare la voce.") Console.WriteLine("Premere 'e' per procedere.") Console.WriteLine() For Each Key As Char In Operations.Keys 'Disegna sullo schermo una casella di spunta, piena: ' [X] 'se l'operazione è attivata, altrimenti vuota: ' [ ] Console.Write("[") If Operations(Key).Enabled = True Then Console.Write("X") Else Console.Write(" ") End If Console.Write("] ") 'Scrive quindi il carattere da premere e vi associa la descrizione Console.Write(Key) Console.Write(" - ") Console.WriteLine(Operations(Key).Description) Next Cmd = Console.ReadKey().KeyChar If Operations.ContainsKey(Cmd) Then Operations(Cmd).Enabled = Not Operations(Cmd).Enabled End If Loop Until Cmd = "e"c Do Console.Clear() Console.WriteLine("Premere la lettera corrispondente per selezionare la voce.") Console.WriteLine("Premere 'e' per procedere.") Console.WriteLine() For Each Key As Char In Conditions.Keys Console.Write("[") If Conditions(Key).Enabled = True Then Console.Write("X") Else Console.Write(" ") End If Console.Write("] ") Console.Write(Key) Console.Write(" - ") Console.WriteLine(Conditions(Key).Description) Next Cmd = Console.ReadKey().KeyChar If Conditions.ContainsKey(Cmd) Then Conditions(Cmd).Enabled = Not Conditions(Cmd).Enabled End If Loop Until Cmd = "e"c Console.Clear() Console.WriteLine("Acquisizione informazioni") Console.WriteLine() 'Cicla su tutte le operazioni presenti nell'Hashtable. For Each Op As Operation In Operations.Values 'Se l'operazione è attivata... If (Op.Enabled) Then 'Se richiede dati ausiliari, invoca il delegate memorizzato 'nella proprietà RequireData. Invoke è un metodo 'di istanza che invoca i metodi contenuti nel delegate. 'Si può anche scrivere: ' Op.RequireData()() 'Dove la prima coppia di parentesi indica che la proprietà 'non è indicizzata e la seconda, in questo caso, specifica 'che il metodo sotteso dal delegate non richiede parametri. 'È più comprensibile la prima forma If Op.RequireData IsNot Nothing Then Op.RequireData.Invoke() End If 'Se DoOperations non contiene ancora nulla, vi inserisce Op.Execute If DoOperations Is Nothing Then DoOperations = Op.Execute Else 'Altrimenti, combina gli oggetti delegate già memorizzati 'con il nuovo DoOperations = System.Delegate.Combine(DoOperations, Op.Execute) End If End If Next For Each C As Condition In Conditions.Values If C.Enabled Then If C.RequireData IsNot Nothing Then C.RequireData.Invoke() End If If VerifyConditions Is Nothing Then VerifyConditions = C.Verify Else VerifyConditions = System.Delegate.Combine(VerifyConditions, C.Verify) End If End If Next FileIndex = 0 For Each File As String In IO.Directory.GetFiles(Folder) 'Ok indica se il file ha passato le condizioni Dim Ok As Boolean = True 'Se ci sono condizioni da applicare, le verifica If VerifyConditions IsNot Nothing Then 'Dato che nel caso di delegate multicast contenenti 'rifermenti a funzione, il valore restituito è 'solo quello della prima funzione e a noi interessano 'tutti i valori restituiti, dobbiamo enumerare 'ogni singolo oggetto delegate presente nel 'delegate multicast e invocarlo singolarmente. 'Ci viene in aiuto il metodo di istanza GetInvocationList, 'che restituisce un array di delegate singoli. For Each C As FileFilter In VerifyConditions.GetInvocationList() 'Tutte le condizioni attive devono essere verificate, 'quindi bisogna usare un And Ok = Ok And C(File) Next End If 'Se le condizioni sono verificate, esegue le operazioni If Ok Then Try DoOperations(File) Catch Ex As Exception Console.WriteLine("Impossibile eseguire l'operazione: " & Ex.Message) End Try End If Next 'Chiude il file di log se era aperto If LogFile IsNot Nothing Then LogFile.Close() End If Console.WriteLine("Operazioni eseguite con successo!") Console.ReadKey() End Sub End ModuleQuesto esempio molto artificioso è solo un assaggio delle potenzialità dei delegate (noterete che ci sono anche molti conflitti, ad esempio se si seleziona sia copia che elimina, i file potrebbero essere cancellati prima della copia a seconda dell'ordine di invocazione). Vedremo fra poco come utilizzare alcuni delegate piuttosto comuni messi a disposizione dal Framework, e scopriremo nella sezione B che i delegate sono il meccanismo fondamentale alla base di tutto il sistema degli eventi.
Alcuni membri importanti per i delegate multicast
La classe System.Delegate espone alcuni metodi statici pubblici, molti dei quali sono davvero utili quando si tratta di delegate multicast. Eccone una breve lista:- Combine(A, B) o Combine(A, B, C, ...) : fonde insieme più delegate per creare un unico delegate multicast invocando il quale vengono invocati tutti i metodi in esso contenuti;
- GetInvocationList() : funzione d'istanza che restituisce un array di oggetti di tipo System.Delegate, i quali rappresentano i singoli delegate che sono stati memorizzati nell'unica variabile
- Remove(A, B) : rimuove l'oggetto delegate B dalla invocation list di A (ossia dalla lista di tutti i singoli delegate memorizzati in A). Si suppone che A sia multicast. Se anche B è multicast, solo l'ultimo elemento dell'invocation list di B viene rimosso da quella di A
- RemoveAll(A, B) : rimuove tutte le occorrenze degli elementi presenti nell'invocation list di B da quella di A. Si suppone che sia A che B siano multicast
A cura di: Il Totem