|
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.
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 Module
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.
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
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:
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 Module
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 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 Module
Ora, è 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().
|
|