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 - Utilizzo delle Interfacce Parte II

Guida al Visual Basic .NET

Capitolo 39° - Utilizzo delle Interfacce Parte II

<< Precedente Prossimo >>


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.
ShallowCopy.jpg
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().

<< 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.