I generics in C#
Nella prima serie abbiamo incontrato collezioni come List<T> e Dictionary<TKey, TValue>, notando quelle parentesi angolari senza soffermarci sul loro significato. Quel meccanismo si chiama generics ed è uno degli strumenti più potenti di C#: permette di scrivere classi e metodi che funzionano con qualsiasi tipo, mantenendo la sicurezza garantita dal compilatore. In questo articolo capiremo come definire tipi e metodi generici e come vincolarli quando necessario.
Il problema che i generics risolvono
Immaginiamo di voler scrivere un contenitore che memorizza un singolo valore. Senza generics, avremmo due opzioni insoddisfacenti: scrivere una classe diversa per ogni tipo, duplicando il codice, oppure usare il tipo base object, perdendo però la sicurezza sui tipi e costringendo a conversioni continue e rischiose.
// Approccio con object: fragile e poco sicuro
class Box
{
public object Value { get; set; }
}
var box = new Box { Value = 42 };
// Serve un cast, che potrebbe fallire a runtime
int number = (int)box.Value;
I generics eliminano questo compromesso: consentono di parametrizzare la classe rispetto a un tipo che verrà specificato al momento dell'uso.
Una classe generica
Per rendere generica una classe si introduce un parametro di tipo tra parentesi angolari dopo il nome. Per convenzione si usa la lettera T, ma qualsiasi identificatore è valido. Quel T funge da segnaposto per il tipo reale, che sarà scelto quando la classe verrà istanziata.
// T e un segnaposto per un tipo qualsiasi
class Box<T>
{
public T Value { get; set; }
public Box(T value)
{
Value = value;
}
}
// Il tipo viene specificato alla creazione
var intBox = new Box<int>(42);
var textBox = new Box<string>("Ciao");
int number = intBox.Value; // nessun cast necessario
string text = textBox.Value; // il tipo e garantito
Il compilatore genera internamente una versione specializzata per ogni tipo utilizzato, offrendo prestazioni ottimali e piena sicurezza: un Box<int> può contenere solo interi, e il tentativo di assegnargli una stringa verrebbe segnalato come errore in fase di compilazione.
Metodi generici
Non è necessario che l'intera classe sia generica: anche un singolo metodo può esserlo, dichiarando il proprio parametro di tipo. Questo è utile per operazioni riutilizzabili che si applicano a tipi diversi.
// Scambia il contenuto di due variabili di qualsiasi tipo
static void Swap<T>(ref T first, ref T second)
{
T temp = first;
first = second;
second = temp;
}
int a = 1, b = 2;
Swap(ref a, ref b); // il compilatore deduce T come int
string x = "primo", y = "secondo";
Swap(ref x, ref y); // qui T e string
Spesso non serve indicare esplicitamente il tipo tra parentesi angolari: il compilatore lo deduce dagli argomenti passati, un processo chiamato inferenza di tipo che rende le chiamate più pulite.
I vincoli sui tipi
A volte un metodo o una classe generica deve poter fare determinate assunzioni sul tipo con cui lavora. Ad esempio, per confrontare due valori occorre che il tipo supporti il confronto. I vincoli, introdotti dalla parola chiave where, limitano i tipi ammessi e sbloccano le operazioni corrispondenti.
// T deve implementare IComparable, quindi supporta CompareTo
static T Max<T>(T first, T second) where T : IComparable<T>
{
return first.CompareTo(second) >= 0 ? first : second;
}
int larger = Max(3, 7); // restituisce 7
string later = Max("abc", "xyz"); // restituisce "xyz"
C# offre diversi tipi di vincolo, che si possono anche combinare:
| Vincolo | Significato |
|---|---|
where T : class | T deve essere un tipo riferimento |
where T : struct | T deve essere un tipo valore |
where T : new() | T deve avere un costruttore senza parametri |
where T : BaseClass | T deve derivare da una classe base |
where T : IInterface | T deve implementare un'interfaccia |
Più parametri di tipo
Una classe o un metodo generico può dichiarare più parametri di tipo, come accade per il dizionario che accetta separatamente il tipo della chiave e quello del valore. In questo modo si modellano relazioni tra tipi diversi mantenendo il tutto generico.
// Una coppia di due valori di tipi potenzialmente diversi
class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
}
var entry = new Pair<string, int>("eta", 30);
Vantaggi dei generics
L'adozione dei generics porta benefici concreti che spiegano la loro presenza pervasiva nella libreria standard:
- Sicurezza sui tipi: gli errori vengono individuati in compilazione, non a runtime.
- Riuso del codice: una sola implementazione serve infiniti tipi, senza duplicazioni.
- Prestazioni: con i tipi valore si evitano le operazioni di boxing e unboxing, ovvero l'incapsulamento in un oggetto necessario quando si usa
object.
Conclusione
Abbiamo scoperto i generics come meccanismo per scrivere codice indipendente dal tipo senza rinunciare alla sicurezza. Abbiamo definito classi e metodi generici, sfruttato l'inferenza di tipo, applicato vincoli con where per abilitare operazioni specifiche e usato più parametri di tipo. Questi concetti sono alla base di gran parte della libreria standard di .NET. Nel prossimo articolo esploreremo delegati, lambda ed eventi, gli strumenti che permettono di trattare i metodi stessi come dati.