Prologo
Lo dico onestamente: mi sono preso il mio tempo
per studiare i generics, ma non ho ancora avuto modo di utilizzarli praticamente
sul campo. Io personalmente consiglio questo documento tratto da MSDN: sono 45 pagine
stampate, ed illustra vantaggi dei generics, come creare
classi generiche, come porre vincoli, come si
comporta Reflection, cosa vuol dire avere method e delegate generici, e così via.
Quindi, piuttosto che affrontare un argomento di questo
calibro con così poca esperienza, preferisco ritardarlo e scrivere qualcosa più
avanti, quando avremo già visto altri concetti più pratici e quando avremo un
bel blocco di C# funzionante e che compila correttamente. Una intro voglio farla
comunque, soprattutto per chi oggi non ha ancora preso in mano VS2005 e magari
non sa di cosa si tratta e vuole partire da zero. Speriamo di riuscire ad
incuriosirvi e a suscitare un po' di interesse verso la programmazione generica.
Introduzione
I generics ci permettono di scrivere classi
generiche, ovvero che potenzialmente possono lavorare su un tipo di dati che non
è definito all'interno della classe stessa, quanto invece dalla classe che na fa
uso. Il documento che ho preso come riferimento fa un esempio molto
semplice, quanto efficace.
Supponiamo di voler implementare una classe Stack, con i
classici metodi Push(x) e Pop(). In questo
caso, abbiamo due diverse alternative, a seconda del tipo di dati che vogliamo
gestire con la nostra classe. Vediamoli con attenzione e capiamo quali sono
vantaggi e svantaggi:
- Io non so a priori quali oggetti andrò a mettere nello
stack. In questo caso, al suo interno la classe fa uso di una
struttura object[]: chiamando il metodo Push(x) un
certo numero di volte, posso memorizzare la prima volta un numero, poi una
stringa, poi una classe e così via. Il metodo Pop() ritorna anch'esso un object, proprio perchè tecnicamente nello stack potrei avere
di tutto.
- Io so che nel mio stack andrò a mettere soltanto un tipo di dati
specifico, ad esempio int. Bene:
l'implementazione farà uso di un int[], il metodo Push
consentirà solamente l'inserimento di int, e il metodo
Pop() ritornerà sempre e soltanto oggetti int.
Diciamo subito che dal punto di vista delle prestazioni, il metodo (2) è
sicuramente più efficace. Il compilatore non ha a che fare con object generici,
ma sa che ha a che fare con int. Quindi, in fase di
compilazione vengono verificati i tipi (strong-typed), ci viene in aiuto
l'Intellisense, le performance sono ottimali. Nel primo caso invece
dobbiamo continuamente castare al tipo di dati corretto (casting a
tutto andare, con boxing/unboxing continui), e di conseguenza potremmo
avere exception a run-time che per definizione non siamo riusciti a prevenire
durante la scrittura del codice.
Adesso facciamo un piccolo esperimento: riprendete i concetti espressi dalla
soluzione (2) ed immaginatevi il codice C# corrispondente. Immaginatevi un int[], un metodo void Push(int Value)
ed un metodo int Pop(). Ora, fate un bel
replace di int con una bella T. Cosa abbiamo ottenuto? Semplice:
la nostra classe Stack è diventata generica, ovvero è in grado
di gestire qualsiasi tipo di dato previsto da .NET. Avremo un T[], avremo un metodo void Push(T
value) ed un metodo T Pop(). T è in pratica un
placeholder, che verrà sostituito in fase di compilazione con il tipo di dati
effettivo.
public class Stack<T>
{
T[] oggetti;
public void Push(T elemento)
{ }
public T Pop()
{ }
}
Il tipo di dato reale viene stabilito dal client di questa classe
Stack: in fase di dichiarazione, possiamo scrivere una cosa del
tipo:
Stack<string> stack = new Stack<string>();
Stack<Book> stack2 = new Stack<Book>();
Stack<int> stack3 = new Stack<int>();
Oltre ai vantaggi citati prima, possiamo dedurne subito un altro: la classe
Stack è fisicamente soltanto una. Nei nostri progetti VS2005,
avremo un solo file Stack.cs, che ha la capacità di gestire -
in base a come viene dichiarata - un tipo piuttosto che un altro. Se trovassimo
un bug, o se volessimo modificare la classe, lo faremmo in un punto solo. Nel
metodo (2) citato sopra, avremmo avuto una classe per ogni tipo di dato, il che
significa duplicazione di codice, scarsa manutenzione, etc. etc.
Generico è bello, ma troppo generico stroppia!
Sembra uno
scioglilingua.
Un'idea che mi sono fatto, confermata
dalle ricerche che ho fatto su google, è che lo scopo principale dei Generics è quello
di creare classi per l'implementazione delle strutture dati di base (stack,
queue, tree, list, collection, etc.): classi che per loro natura possono e
devono essere utili indipendentemente da cosa contengono, anzi...il cui scopo
principale è quello di contenere.
Beh, io sarò anche un Igor qualunque, ma c'è qualcosa che non mi convince. Il
"troppo generico" è troppo estremista, e ci impedisce di scrivere funzionalità
davvero utili. Faccio un esempio: all'interno di una classe generica, non
possiamo usare l'operatore '+', perchè è un'operatore che in alcuni casi
potrebbe non funzionare, o non avere senso. Nella scrittura del codice in una
classe generica, il tipo T viene visto come object, e quindi non abbiamo
proprietà o metodi specifici. Mi si sono aperti gli occhi però quando ho visto
l'utilizzo della keywork where, che ci permette di creare
constraints (vincoli, per dirla all'italiana) sul tipo, obbligandolo per esempio
a dover implementare una determinata interfaccia. Se noi sappiamo che il tipo T
implementa l'interfaccia ICollection, sappiamo che dispone di una property
Count, e così via per tutte le interfacce di .NET e quelle
eventualmente implementate da noi nel nostro codice.
Quindi, mi piace pensare che i Generics possano in qualche modo essere utili
nel caso in cui abbia un set di classi (leggesi: Book, Magazine, Newspaper,
Journal) che implementano tutte una certa interfaccia (leggesi:
IReadingOnPaper), e che quindi so per definizione che rispondono a determinati
requisiti (leggesi: .Title, .Author, .InLoan, etc), indipendentemente dal
tipo.