Guida al Visual Basic .NET
Capitolo 39° - Utilizzo delle Interfacce Parte II
IEnumerable e IEnumeratorUna classe che implementa IEnumerable diventa enumerabile agli occhi del .NET Framework: ciò significa che si può usare su di essa un costrutto For Each per scorrerne tutti gli elementi. Di solito questo tipo di classe rappresenta una collezione di elementi e per questo motivo il suo nome, secondo le convenzioni, dovrebbe terminare in "Collection". Un motivo per costruire una nuova collezione al posto di usare le classiche liste può consistere nel voler definire nuovi metodi o proprietà per modificarla. Ad esempio, si potrebbe scrivere una nuova classe PersonCollection che permette di raggruppare ed enumerare le persone ivi contenute e magari calcolare anche l'età media.Module Module1 Class PersonCollection Implements IEnumerable 'La lista delle persone Private _Persons As New ArrayList 'Teoricamente, si dovrebbero ridefinire tutti i metodi 'di una collection comune, ma per mancanza di spazio, 'accontentiamoci Public ReadOnly Property Persons() As ArrayList Get Return _Persons End Get End Property 'Restituisce l'età media. TimeSpan è una struttura che si 'ottiene sottraendo fra loro due oggetti date e indica un 'intervallo di tempo Public ReadOnly Property AverageAge() As String Get 'Variabile temporanea Dim Temp As TimeSpan 'Somma tutte le età For Each P As Person In _Persons Temp = Temp.Add(Date.Now - P.BirthDay) Next 'Divide per il numero di persone Temp = TimeSpan.FromSeconds(Temp.TotalSeconds / _Persons.Count) 'Dato che TimeSpan può contenere al massimo 'giorni e non mesi o anni, dobbiamo fare qualche 'calcolo Dim Years As Int32 'Gli anni, ossia il numero dei giorni fratto 365 'Divisione intera Years = Temp.TotalDays 365 'Sottrae gli anni: da notare che '(Temp.TotalDays 365) * 365) non è un passaggio 'inutile. Infatti, per determinare il numero di 'giorni che rimangono, bisogna prendere la 'differenza tra il numero totale di giorni e 'il multiplo più vicino di 365 Temp = _ Temp.Subtract(TimeSpan.FromDays((Temp.TotalDays 365) * 365)) Return Years & " anni e " & CInt(Temp.TotalDays) & " giorni" End Get End Property 'La funzione GetEnumerator restituisce un oggetto di tipo 'IEnumerator che vedremo fra breve: esso permette di 'scorrere ogni elemento ordinatamente, dall'inizio 'alla fine. In questo caso, poichè non abbiamo ancora 'analizzato questa interfaccia, ci limitiamo a restituisce 'l'IEnumerator predefinito per un ArrayList Public Function GetEnumerator() As IEnumerator _ Implements IEnumerable.GetEnumerator Return _Persons.GetEnumerator End Function End Class Sub Main() Dim Persons As New PersonCollection With Persons.Persons .Add(New Person("Marcello", "Rossi", Date.Parse("10/10/1992"))) .Add(New Person("Guido", "Bianchi", Date.Parse("01/12/1980"))) .Add(New Person("Bianca", "Brega", Date.Parse("23/06/1960"))) .Add(New Person("Antonio", "Felice", Date.Parse("16/01/1930"))) End With For Each P As Person In Persons Console.WriteLine(P.CompleteName) Next Console.WriteLine("Età media: " & Persons.AverageAge) '> 41 anni e 253 giorni Console.ReadKey() End Sub End ModuleCome si vede dall'esempio, è lecito usare PersonCollection nel costrutto For Each: l'iterazione viene svolta dal primo elemento inserito all'ultimo, poichè l'IEnumerator dell'ArrayList opera in questo modo. Tuttavia, creando una diversa classe che implementa IEnumerator si può scorrere la collezione in qualsiasi modo: dal più giovane al più vecchio, al primo all'ultimo, dall'ultimo al primo, a caso, saltandone alcuni, a seconda dell'ora di creazione eccetera. Quindi in questo modo si può personalizzare la propria collezione. Ciò che occorre per costruire correttamente una classe basata su IEnumerator sono tre metodi fondamentali definiti nell'interfaccia: MoveNext è una funzione che restituisce True se esiste un elemento successivo nella collezione (e in questo caso lo imposta come elemento corrente), altrimenti False; Current è una proprietà ReadOnly di tipo Object che restituisce l'elemento corrente; Reset è una procedura senza parametri che resetta il contatore e fa iniziare il ciclo daccapo. Quest'ultimo metodo non viene mai utilizzato, ma nell'esempio che segue ne scriverò comunque il corpo: Module Module1 Class PersonCollection Implements IEnumerable 'La lista delle persone Private _Persons As New ArrayList 'Questa classe ha il compito di scorrere ordinatamente gli 'elementi della lista, dal più vecchio al più giovane Private Class PersonAgeEnumerator Implements IEnumerator 'Per enumerare gli elementi, la classe ha bisogno di un 'riferimento ad essi: perciò si deve dichiarare ancora 'un nuovo ArrayList di Person. Questo passaggio è 'facoltativo nelle classi nidificate come questa, ma è 'obbligatorio in tutti gli altri casi Private Persons As New ArrayList 'Per scorrere la collezione, si userà un comune indice Private Index As Int32 'Essendo una normalissima classe, è lecito definire un 'costruttore, che in questo caso inizializza la 'collezione Sub New(ByVal Persons As ArrayList) 'Ricordate: poichè ArrayList deriva da Object, è 'un tipo reference. Assegnare Persons a Me.Persons 'equivale ad assegnarne l'indirizzo e quindi ogni 'modifica su questo arraylist privato si rifletterà 'su quello passato come parametro. Si può 'evitare questo problema clonando la lista Me.Persons = Persons.Clone 'MoveNext viene richiamato prima di usare Current, 'quindi Index verrà incrementata subito. 'Per farla diventare 0 al primo ciclo la si 'deve impostare a -1 Index = -1 'Dato che l'enumeratore deve scorrere la lista 'secondo l'anno di nascita, bisogna prima ordinarla Me.Persons.Sort(New BirthDayComparer) End Sub 'Restituisce l'elemento corrente Public ReadOnly Property Current() As Object _ Implements System.Collections.IEnumerator.Current Get Return Persons(Index) End Get End Property 'Restituisce True se esiste l'elemento successivo e lo 'imposta, altrimenti False Public Function MoveNext() As Boolean _ Implements System.Collections.IEnumerator.MoveNext If Index = Persons.Count - 1 Then Return False Else Index += 1 Return True End If End Function 'Resetta il ciclo Public Sub Reset() _ Implements System.Collections.IEnumerator.Reset Index = -1 End Sub End Class Public ReadOnly Property Persons() As ArrayList Get Return _Persons End Get End Property Public ReadOnly Property AverageAge() As String Get Dim Temp As TimeSpan For Each P As Person In _Persons Temp = Temp.Add(Date.Now - P.BirthDay) Next Temp = TimeSpan.FromSeconds(Temp.TotalSeconds / _Persons.Count) Dim Years As Int32 Years = Temp.TotalDays 365 Temp = _ Temp.Subtract(TimeSpan.FromDays((Temp.TotalDays 365) * 365)) Return Years & " anni e " & CInt(Temp.TotalDays) & " giorni" End Get End Property 'La funzione GetEnumerator restituisce ora un oggetto di 'tipo IEnumerator che abbiamo definito in una classe 'nidificata e il ciclo For Each scorrerà quindi 'dal più vecchio al più giovane Public Function GetEnumerator() As IEnumerator _ Implements IEnumerable.GetEnumerator Return New PersonAgeEnumerator(_Persons) End Function End Class Sub Main() Dim Persons As New PersonCollection With Persons.Persons .Add(New Person("Marcello", "Rossi", Date.Parse("10/10/1992"))) .Add(New Person("Guido", "Bianchi", Date.Parse("01/12/1980"))) .Add(New Person("Bianca", "Brega", Date.Parse("23/06/1960"))) .Add(New Person("Antonio", "Felice", Date.Parse("16/01/1930"))) End With 'Enumera ora per data di nascita, ma senza modificare 'l'ordine degli elementi For Each P As Person In Persons Console.WriteLine(P.BirthDay.ToShortDateString & ", " & _ P.CompleteName) Next 'Stampa la prima persona, dimostrando che l'ordine 'della lista è intatto Console.WriteLine(Persons.Persons(0).CompleteName) Console.ReadKey() End Sub End Module ICloneableCome si è visto nell'esempio appena scritto, si presentano alcune difficoltà nel manipolare oggetti di tipo Reference, in quanto l'assegnazione di questi creerebbe due istanze che puntano allo stesso oggetto piuttosto che due oggetti distinti. È in questo tipo di casi che il metodo Clone e l'interfaccia ICloneable assumono un gran valore. Il primo permette di eseguire una copia dell'oggetto, creando un nuovo oggetto a tutti gli effetti, totalmente disgiunto da quello di partenza: questo permette di non intaccarne accidentalmente l'integrità. Una dimostrazione:Module Esempio Sub Main() 'Il tipo ArrayList espone il metodo Clone Dim S1 As New ArrayList Dim S2 As New ArrayList S2 = S1 'Verifica che S1 e S2 puntano lo stesso oggetto Console.WriteLine(S1 Is S2) '> True 'Clona l'oggetto S2 = S1.Clone 'Verifica che ora S2 referenzia un oggetto differente, 'ma di valore identico a S1 Console.WriteLine(S1 Is S2) '> False Console.ReadKey() End Sub End ModuleL'interfaccia, invece, come accadeva per IEnumerable e IComparable, indica al .NET Framework che l'oggetto è clonabile. Questo codice mostra la funzione Close all'opera: Module Module1 Class UnOggetto Implements ICloneable Private _Campo As Int32 Public Property Campo() As Int32 Get Return _Campo End Get Set(ByVal Value As Int32) _Campo = Value End Set End Property 'Restituisce una copia dell'oggetto Public Function Clone() As Object Implements ICloneable.Clone 'La funzione Protected MemberwiseClone, ereditata da 'Object, esegue una copia superficiale dell'oggetto, 'come spiegherò fra poco: è quello che 'serve in questo caso Return Me.MemberwiseClone End Function 'L'operatore = permette di definire de due oggetti hanno un 'valore uguale Shared Operator =(ByVal O1 As UnOggetto, ByVal O2 As UnOggetto) As _ Boolean Return O1.Campo = O2.Campo End Operator Shared Operator <>(ByVal O1 As UnOggetto, ByVal O2 As UnOggetto) As _ Boolean Return Not (O1 = O2) End Operator End Class Sub Main() Dim O1 As New UnOggetto Dim O2 As UnOggetto = O1.Clone 'I due oggetti NON sono lo stesso oggetto: il secondo 'è solo una copia, disgiunta da O1 Console.WriteLine(O1 Is O2) '> False 'Tuttavia hanno lo stesso identico valore Console.WriteLine(O1 = O2) '> True Console.ReadKey() End Sub End ModuleOra, è importante distinguere due tipi di copia: quella Shallow e quella Deep. La prima crea una copia superficiale dell'oggetto, ossia si limita a clonare tutti i campi. La seconda, invece, è in grado di eseguire questa operazione anche su tutti gli oggetti interni e i riferimenti ad altri oggetti: così, se si ha una classe Person che al proprio interno contiene il campo Childern, di tipo array di Person, la copia Shallow creerà un clone della classe in cui Children punta sempre allo stesso oggetto, mentre una copia Deep clonerà anche Children. Si nota meglio con un grafico: le frecce verdi indicano oggetti clonati, mentre la freccia arancio si riferisce allo stesso oggetto. Non è possibile specificare nella dichiarazione di Clone quale tipo di copia verrà eseguita, quindi tutto viene lasciato all'arbitrio del programmatore. Dal codice sopra scritto, si nota che Clone deve restituire per forza un tipo Object. In questo caso, il metodo si dice a tipizzazione debole, ossia serve un operatore di cast per convertirlo nel tipo desiderato; per crearne una versione a tipizzazione forte è necessario scrivere una funzione che restituisca, ad esempio, un tipo Person. Quest'ultima versione avrà il nome Clone, mentre quella che implementa ICloneable.Clone() avrà un nome differente, come CloneMe().
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|