A38. Utilizzo delle Interfacce, Parte II
IEnumerable e IEnumerator
Una 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.
Come 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.Module Module1Class PersonCollectionImplements IEnumerable 'La lista delle persone Private _PersonsAs New ArrayList 'Teoricamente, si dovrebbero ridefinire tutti i metodi di una 'collection comune, ma per mancanza di spazio, accontentiamoci Public ReadOnly Property Persons()As ArrayListGet Return _PersonsEnd 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 TempAs TimeSpan 'Somma tutte le età For Each PAs PersonIn _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 YearsAs 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.GetEnumeratorReturn _Persons.GetEnumeratorEnd Function End Class Sub Main()Dim PersonsAs New PersonCollectionWith 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 PAs PersonIn Persons Console.WriteLine(P.CompleteName)Next Console.WriteLine("Età media: " & Persons.AverageAge) '> 41 anni e 253 giorni Console.ReadKey()End Sub End Module
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 Module1Class PersonCollectionImplements IEnumerable 'La lista delle persone Private _PersonsAs New ArrayList 'Questa classe ha il compito di scorrere ordinatamente gli 'elementi della lista, dal più vecchio al più giovane Private Class PersonAgeEnumeratorImplements 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 PersonsAs New ArrayList 'Per scorrere la collezione, si userà un comune indice Private IndexAs Int32 'Essendo una normalissima classe, è lecito definire un 'costruttore, che in questo caso inizializza la collezione Sub New (ByVal PersonsAs 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 problemo 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.CurrentGet 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.MoveNextIf Index = Persons.Count - 1Then Return FalseElse Index += 1Return TrueEnd If End Function 'Resetta il ciclo Public Sub Reset() _Implements System.Collections.IEnumerator.Reset Index = -1End Sub End Class Public ReadOnly Property Persons()As ArrayListGet Return _PersonsEnd Get End Property Public ReadOnly Property AverageAge()As String Get Dim TempAs TimeSpanFor Each PAs PersonIn _Persons Temp = Temp.Add(Date .Now - P.BirthDay)Next Temp = TimeSpan.FromSeconds(Temp.TotalSeconds / _Persons.Count)Dim YearsAs 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.GetEnumeratorReturn New PersonAgeEnumerator(_Persons)End Function End Class Sub Main()Dim PersonsAs New PersonCollectionWith 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 PAs PersonIn 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
ICloneable
Come 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:
L'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 EsempioSub Main() 'Il tipo ArrayList espone il metodo Clone Dim S1As New ArrayListDim S2As New ArrayList S2 = S1 'Verifica che S1 e S2 puntano lo stesso oggetto Console.WriteLine(S1Is S2) '> True 'Clona l'oggetto S2 = S1.Clone 'Verifica che ora S2 referenzia un oggetto differente, 'ma di valore identico a S1 Console.WriteLine(S1Is S2) '> False Console.ReadKey()End Sub End Module
Ora, è importante distinguere due tipo 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, anch'esso di tipo Person, la copia Shallow creerà un clone della classe in cui Children punta sempre allo stesso oggetto, mentre una copia Deep clonerà anche Children. Non è possibile specificare nella dichiarazione di Clone quale tipo di copia verrà eseguita, quindi tutto viene lasciato all'arbitrio del programmatore.Class UnOggettoImplements ICloneablePrivate _CampoAs Int32Public Property Campo()As Int32Get Return _CampoEnd Get Set (ByVal ValueAs Int32) _Campo = ValueEnd 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 .MemberwiseCloneEnd Function 'L'operatore = permette di definire de due oggetti hanno un 'valore uguale Shared Operator =(ByVal O1As UnOggetto,ByVal O2As UnOggetto)As _Boolean Return O1.Campo = O2.CampoEnd OperatorShared Operator <>(ByVal O1As UnOggetto,ByVal O2As UnOggetto)As _Boolean Return Not (O1 = O2)End OperatorEnd Class Sub Main()Dim O1As New UnOggettoDim O2As UnOggetto = O1.Clone 'I due oggetti NON sono lo stesso oggetto: il secondo è solo 'una copia, disgiunta da O1 Console.WriteLine(O1Is O2) '> False 'Tuttavia hanno lo stesso identico valore Console.WriteLine(O1 = O2) '> True Console.ReadKey()End Sub End Module
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().
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



