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