Oppure

Loading


Panoramica sui Generics

I Generics sono un concetto molto importante per quanto riguarda la programmazione ad oggetti, specialmente in .NET e, se fino ad ora non ne conoscevate nemmeno l'esistenza, d'ora in poi non potrete farne a meno. Cominciamo col fare un paragone per esemplificare il concetto di generics. Ammettiamo di dichiarare una variabile I di tipo Int32: in questa variabile potremo immagazzinare qualsiasi informazione che consista di un numero intero rappresentabile su 32 bit. Possiamo dire, quindi, che il tipo Int32 costituisce un'astrazione di tutti i numeri interi esistenti da -2'147'483'648 a +2'147'483'647. Analogamente un tipo generic può assumere come valore un altro tipo e, quindi, astrae tutti i possibili tipi usabili in quella classe/metodo/proprietà eccetera. È come dire: definiamo la funzione Somma(A, B), dove A e B sono di un tipo T che non conosciamo. Quando utilizziamo la funzione Somma, oltre a specificare i parametri richiesti, dobbiamo anche "dire" di quale tipo essi siano (ossia immettere in T non un valore ma un tipo): in questo modo, definendo un solo metodo, potremo eseguire somme tra interi, decimali, stringhe, date, file, classi, eccetera... In VB.NET, l'operazione di specificare un tipo per un entità generic si attua con questa sintassi:
[NomeEntità](Of [NomeTipo])
Dato i generics di possono applicare ad ogni entità del .NET (metodi, classi, proprietà, strutture, interfacce, delegate, eccetera...), ho scritto solo "NomeEntità" per indicare il nome del target a cui si applicano. Il prossimo esempio mostra come i generics, usati sulle liste, possano aumentare di molto le performance di un programma.
La collezione ArrayList, molte volte impiegata negli esempi dei precedeti capitoli, permette di immagazzinare qualsiasi tipo di dato, memorizzando, quindi, variabili di tipo Object. Come già detto all'inizio del corso, l'uso di Object comporta molti rischi sia a livello di prestazioni, dovute alle continue operazioni di boxing e unboxing (e le garbage collection che ne conseguono, data la creazione di molti oggetti temporanei), sia a livello di correttezza del codice. Un esempio di questo ultimo caso si verifica quando si tenta di scorrere un ArrayList mediante un ciclo For Each e si incontra un record che non è del tipo specificato, ad esempio:
Dim A As New ArrayList
A.Add(2)
A.Add(3)
A.Add("C") 
'A run-time, sarà lanciata un'eccezione inerente il cast
'poichè la stringa "C" non è del tipo specificato 
'nel blocco For Each
For Each V As Int32 In A
  Console.WriteLine(V)
Next 
Infatti, se l'applicazione dovesse erroneamente inserire una stringa al posto di un numero intero, non verrebbe generato nessun errore, ma si verificherebbe un'eccezione successivamente. Altra problematica legata all'uso di collezioni a tipizzazione debole (ossia che registrano generici oggetti Object, come l'ArrayList, l'HashTable o la SortedList) è dovuta al fatto che sia necessaria una conversione esplicita di tipo nell'uso dei suoi elementi, almeno nella maggioranza dei casi. La soluzione adottata da un programmatore che non conoscesse i generics per risolvere tali inconvenienti sarebbe quella di creare una nuova lista, ex novo, ereditandola da un tipo base come CollectionBase e ridefinendone tutti i metodi (Add, Remove, IndexOf ecc...). L'uso dei Generics, invece, rende molto più veloce e meno insidiosa la scrittura di un codice robusto e solido nell'ambito non solo delle collezioni, ma di molti altri argomenti. Ecco un esempio di come implementare una soluzione basata sui Generics:
'La lista accetta solo oggetti di tipo Int32: per questo motivo
'si genera un'eccezione quando si tenta di inserirvi elementi di
'tipo diverso e la velocità di elaborazione aumenta!
Dim A As New List(Of Int32)
A.Add(1)
A.Add(4)
A.Add(8)
'A.Add("C") '<- Impossibile
For Each V As Int32 In A
  Console.WriteLine(V)
Next 
E questa è una dimostrazione dell'incremento delle prestazioni:
Module Module1
    Sub Main()
        Dim TipDebole As New ArrayList
        Dim TipForte As New List(Of Int32)
        Dim S As New Stopwatch

        'Cronometra le operazioni su ArrayList
        S.Start()
        For I As Int32 = 1 To 1000000
            TipDebole.Add(I)
        Next
        S.Stop()
        Console.WriteLine(S.ElapsedMilliseconds & _ 
            " millisecondi per ArrayList!")

        'Cronometra le operazioni su List
        S.Reset()
        S.Start()
        For I As Int32 = 1 To 1000000
            TipForte.Add(I)
        Next
        S.Stop()
        Console.WriteLine(S.ElapsedMilliseconds & _ 
            " millisecondi per List(Of T)!")

        Console.ReadKey()
    End Sub
End Module 
Sul mio computer portatile l'ArrayList impiega 197ms, mentre List 33ms: i Generics incrementano la velocità di 6 volte!
Oltre a List, esistono anche altre collezioni generic, ossia Dictionary e SortedDictionary: tutti questi sono la versione a tipizzazione forte delle normali collezioni già viste. Ma ora vediamo come scrivere nuove classi e metodi generic.


Generics Standard

Una volta imparato a dichiarare e scrivere entità generics, sarà anche altrettanto semplice usare quelli esistenti, perciò iniziamo col dare le prime informazioni su come scrivere, ad esempio, una classe generics.
Una classe generics si riferisce ad un qualsiasi tipo T che non possiamo conoscere al momento dela scrittura del codice, ma che il programmatore specificherà all'atto di dichiarazione di un oggetto rappresentato da questa classe. Il fatto che essa sia di tipo generico indica che anche i suoi membri, molto probabilmente, avranno lo stesso tipo: più nello specifico, potrebbero esserci campi di tipo T e metodi che lavorano su oggetti di tipo T. Se nessuna di queste due condizioni è verificata, allora non ha senso scrivere una classe generics. Ma iniziamo col vedere un semplice esempio:
Module Module1
    'Collezione generica che contiene un qualsiasi tipo T di
    'oggetto. T si dice "tipo generic aperto"
    Class Collection(Of T)
        'Per ora limitiamoci a dichiarare un array interno
        'alla classe.
        'Vedremo in seguito che è possibile ereditare da
        'una collezione generics già esistente.
        'Notate che la variabile è di tipo T: una volta che
        'abbiamo dichiarato la classe come generics su un tipo T,
        'è come se avessimo "dichiarato" l'esistenza di T
        'come tipo fittizio. 
        Private _Values() As T

        'Restituisce l'Index-esimo elemento di Values (anch'esso
        'è di tipo T)
        Public Property Values(ByVal Index As Int32) As T
            Get
                If (Index >= 0) And (Index < _Values.Length) Then
                    Return _Values(Index)
                Else
                    Throw New IndexOutOfRangeException()
                End If
            End Get
            Set(ByVal value As T)
                If (Index >= 0) And (Index < _Values.Length) Then
                    _Values(Index) = value
                Else
                    Throw New IndexOutOfRangeException()
                End If
            End Set
        End Property

        'Proprietà che restituiscono il primo e l'ultimo
        'elemento della collezione
        Public ReadOnly Property First() As T
            Get
                Return _Values(0)
            End Get
        End Property

        Public ReadOnly Property Last() As T
            Get
                Return _Values(_Values.Length - 1)
            End Get
        End Property

        'Stampa tutti i valori presenti nella collezione a schermo.
        'Su un tipo generic è sempre possibile usare
        'l'operatore Is (ed il suo corrispettivo IsNot) e 
        'confrontarlo con Nothing. Se si tratta di un tipo value
        'l'uguaglianza con Nothing sarà sempre falsa.
        Public Sub PrintAll()
            For Each V As T In _Values
                If V IsNot Nothing Then
                    Console.WriteLine(V.ToString())
                End If
            Next
        End Sub

        'Inizializza la collezione con Count elementi, tutti del
        'valore DefaultValue
        Sub New(ByVal Count As Int32, ByVal DefaultValue As T)
            If Count < 1 Then
                Throw New ArgumentOutOfRangeException()
            End If

            ReDim _Values(Count - 1)

            For I As Int32 = 0 To _Values.Length - 1
                _Values(I) = DefaultValue
            Next
        End Sub

    End Class

    Sub Main()
        'Dichiara quattro variabili contenenti quattro nuovi
        'oggetti Collection. Ognuno di questi, però, 
        'è specifico per un solo tipo che decidiamo 
        'noi durante la dichiarazione. String, Int32, Date 
        'e Person, ossia i tipi che stiamo inserendo nel tipo 
        'generico T, si dicono "tipi generic collegati",
        'poiché collegano il tipo fittizio T con un
        'reale tipo esistente
        Dim Strings As New Collection(Of String)(10, "null")
        Dim Integers As New Collection(Of Int32)(5, 12)
        Dim Dates As New Collection(Of Date)(7, Date.Now)
        Dim Persons As New Collection(Of Person)(10, Nothing)

        Strings.Values(0) = "primo"
        Integers.Values(3) = 45
        Dates.Values(6) = New Date(2009, 1, 1)
        Persons.Values(3) = New Person("Mario", "Rossi", Dates.Last)

        Strings.PrintAll()
        Integers.PrintAll()
        Dates.PrintAll()
        Persons.PrintAll()

        Console.ReadKey()
    End Sub

End Module
Ognuna della quattro variabili del sorgente contiene un oggetto di tipo Collection, ma tali oggetti non sono dello stesso tipo, poiché ognuno espone un differente tipo generics collegato. Quindi, nonostante si tratti sempre della stessa classe Collection, Collection(Of Int32) e Collection(Of String) sono a tutti gli effetti due tipi diversi: è come se esistessero due classi in cui T è sostituito in una da Int32 e nell'altra da String. Per dimostrare la loro diversità, basta scrivere:
Console.WriteLine(Strings.GetType() Is Integers.GetType())
'Output : False


Metodi Generics e tipi generics collegati impliciti

Se si decide di scrivere un solo metodo generics, e di focalizzare su di esso l'attenzione, solo accanto al suo nome apparirà la dichiarazione di un tipo generics aperto, con la consueta clausola "(Of T)". Anche se fin'ora ho usato come nome solamente T, nulla vieta di specificare un altro identificatore valido (ad esempio Pippo): tuttavia, è convenzione che il nome dei tipi generics aperti sia Tn (con n numero intero, ad esempio T1, T2, T3, eccetra...) o, in caso contrario, che inizi almeno con la lettera T (ad esempio TSize, TClass, eccetera...).
Sub [NomeProcedura](Of T)([Parametri])
    '...
End Sub

Function [NomeFunzione](Of T)([Parametri]) As [TipoRestituito]
    '...
End Function
Ecco un semplice esempio:
Module Module1

    'Scambia i valori di due variabili, passate
    'per indirizzo
    Public Sub Swap(Of T)(ByRef Arg1 As T, ByRef Arg2 As T)
        Dim Temp As T = Arg1
        Arg1 = Arg2
        Arg2 = Temp
    End Sub

    Sub Main()
        Dim X, Y As Double
        Dim Z As Single
        Dim A, B As String

        X = 90.0
        Y = 67.58
        Z = 23.01
        A = "Ciao"
        B = "Mondo"

        'Nelle prossime chiamate, Swap non presenta un
        'tipo generics collegato: il tipo viene dedotto dai
        'tipi degli argomenti
        
        'X e Y sono Double, quindi richiama il metodo con
        'T = Double
        Swap(X, Y)
        'A e B sono String, quindi richiama il metodo con
        'T = String
        Swap(A, B)
        
        'Qui viene generato un errore: nonostante Z sia
        'convertibile in Double implicitamente senza perdita
        'di dati, il suo tipo non corrisponde a quello di X,
        'dato che c'è un solo T, che può assumere
        'un solo valore-tipo. Per questo è necessario 
        'utilizzare una scappatoia
        'Swap(Z, X)
        
        'Soluzione 1: si esplicita il tipo generic collegato
        Swap(Of Double)(Z, X)
        'Soluzione 2: si converte Z in double esplicitamente
        Swap(CDbl(Z), X)

        Console.ReadKey()
    End Sub
End Module 


Generics multipli

Quando, anziché un solo tipo generics, se ne specificano due o più, si parla di genrics multipli. La dichiarazione avviene allo stesso modo di come abbiamo visto precedentemente e i tipi vengono separati da una virgola:
Module Module2
    'Una relazione qualsiasi fra due oggetti di tipo indeterminato
    Public Class Relation(Of T1, T2)
        Private Obj1 As T1
        Private Obj2 As T2

        Public ReadOnly Property FirstObject() As T1
            Get
                Return Obj1
            End Get
        End Property

        Public ReadOnly Property SecondObject() As T2
            Get
                Return Obj2
            End Get
        End Property

        Sub New(ByVal Obj1 As T1, ByVal Obj2 As T2)
            Me.Obj1 = Obj1
            Me.Obj2 = Obj2
        End Sub
    End Class

    Sub Main()
        'Crea una relazione fra uno studente e un insegnante,
        'utilizzando le classi create nei capitoli precedenti
        Dim R As Relation(Of Student, Teacher)
        Dim S As New Student("Pinco", "Pallino", Date.Parse("25/06/1990"), _
            "Liceo Scientifico N. Copernico", 4)
        Dim T As New Teacher("Mario", "Rossi", Date.Parse("01/07/1950"), _
        "Matematica")

        'Crea una nuova relazione tra lo studente e l'insegnante
        R = New Relation(Of Student, Teacher)(S, T)
        Console.WriteLine(R.FirstObject.CompleteName)
        Console.WriteLine(R.SecondObject.CompleteName)

        Console.ReadKey()
    End Sub
End Module
Notate che è anche possibile creare una relazione tra due relazioni (e la cosa diventa complicata):
Dim S As New Student("Pinco", "Pallino", Date.Parse("25/06/1990"), "Liceo Scientifico N. Copernico", 4)
Dim T As New Teacher("Mario", "Rossi", Date.Parse("01/07/1950"), "Matematica")
Dim StudentTeacherRelation As Relation(Of Student, Teacher)
Dim StudentClassRelation As Relation(Of Student, String)
Dim Relations As Relation(Of Relation(Of Student, Teacher), Relation(Of Student, String))

StudentTeacherRelation = New Relation(Of Student, Teacher)(S, T)
StudentClassRelation = New Relation(Of Student, String)(S, "5A")
Relations = New Relation(Of Relation(Of Student, Teacher), Relation(Of Student, String))(StudentTeacherRelation, StudentClassRelation)

'Relations.FirstObject.FirstObject
' > Student "Pinco Pallino"
'Relations.FirstObject.SecondObject
' > Teacher "Mario Rossi"
'Relations.SecondObject.FirstObject
' > Student "Pinco Pallino"
'Relations.SecondObject.SecondObject
' > String "5A"


Alcune regole per l'uso dei Generics

  • Si può sempre assegnare Nothing a una variabile di tipo generics. Nel caso il tipo generics collegato sia reference, alla variabile verrà assegnato normalmente Nothing; in caso contrario, essa assumerà il valore di default per il tipo;
  • Non si può ereditare da un tipo generic aperto:
    Class Example(Of T)
        Inherits T
        ' SBAGLIATO
    End Class
    Tuttavia si può ereditare da una classe generics specificando come tipo generics collegato lo stesso tipo aperto:
    Class Example(Of T)
        Inherits List(Of T)
        ' CORRETTO
    End Class
  • Allo stesso modo, non si può implementare T come se fosse un'interfaccia:
    Class Example(Of T)
        Implements T
        ' SBAGLIATO
    End Class
    Ma si può implementare un'interfaccia generics di tipo T:
    Class Example(Of T)
        Implements IEnumerable(Of T)
        ' CORRETTO
    End Class
  • Entità con lo stesso nome ma con generics aperti differenti sono considerate in overload. Pertanto, è lecito scrivere:
    Sub Example(Of T)(ByVal A As T)
        '...
    End Sub
    
    Sub Example(Of T1, T2)(ByVal A As T1)
        '...
    End Sub
A cura di: Il Totem