Menu

Generics in C#: classi generiche, metodi generici e vincoli

Come funzionano i generics in C#: scrivere classi e metodi generici con parametri di tipo, inferenza del tipo, vincoli con where (class, struct, new(), classi base, interfacce), default(T), membri statici per tipo e covarianza con IEnumerable<out T>.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

I generics ti permettono di scrivere un tipo o un metodo una volta sola e usarlo con molti tipi, senza perdere la sicurezza dei tipi. Invece di una List di object che richiede cast e accetta qualsiasi cosa, ottieni List<int> e List<Order>, ciascuna controllata dal compilatore. Il segnaposto tra parentesi angolari, per convenzione T, è un parametro di tipo; il tipo fornito da chi lo usa è l'argomento di tipo.

Perché i generics: il problema che risolvono

Prima dei generics (C# 1), le collezioni memorizzavano object. Il codice compilava, ma gli errori emergevano solo a runtime, e ogni int subiva il boxing all'inserimento:

var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50");                  // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
    total += (decimal)p;              // InvalidCastException on the string

Con List<decimal>, il secondo Add è un errore di compilazione, il ciclo non ha bisogno di cast e i decimal vengono memorizzati come decimal.

Classi generiche

Dichiara i parametri di tipo dopo il nome della classe, poi usali come qualsiasi altro tipo dentro la classe:

Output:

Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False

Page<string> e Page<int> sono due tipi diversi prodotti da un'unica definizione. Una classe può avere più parametri di tipo (Dictionary<TKey, TValue>); chiamali T quando ce n'è uno e TSomething quando sono più di uno.

Metodi generici e inferenza del tipo

Un metodo può avere i propri parametri di tipo, che la sua classe sia generica o no. Chi chiama di solito omette l'argomento di tipo, perché il compilatore lo deduce dagli argomenti:

Output:

2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR

L'inferenza funziona solo a partire dagli argomenti, mai dal tipo di ritorno. Repeat(0, 3) non ha bisogno di annotazioni, ma un metodo come T Create<T>() non ha niente da cui dedurre, quindi chi chiama deve scrivere Create<Customer>(). Gran parte di LINQ è fatta di metodi generici che si basano su questa inferenza: prices.Where(p => p > 1) è Where<decimal> senza che tu debba scriverlo.

Vincoli: cosa può essere T

Dentro un metodo generico, il compilatore ti lascia fare solo ciò che funziona per ogni possibile T. Senza vincoli, è poco più di quello che supporta object. Per chiamare CompareTo, creare un'istanza o leggere una proprietà, di' al compilatore cosa deve essere T, con where:

Output:

17
plum
19.99
42 Zoe

I vincoli che userai:

VincoloSignificatoTi permette di
where T : classtipo riferimentoassegnare null, usare as
where T : structtipo valore non nullableusare T? come Nullable<T>
where T : new()costruttore pubblico senza parametrichiamare new T() (va messo per ultimo)
where T : Entityderiva da Entityusare i membri di Entity
where T : IComparable<T>implementa l'interfacciachiamare CompareTo
where T : Uderiva da un altro parametro di tipomettere in relazione due parametri
where T : unmanaged (C# 7.3)struct senza riferimenti al suo internopuntatori, stackalloc
where T : Enum (C# 7.3)un tipo enumpassarlo ai metodi di Enum
where T : notnull (C# 8)tipo non nullableannotazioni dei tipi riferimento nullable

Più vincoli sullo stesso parametro si separano con virgole; più parametri hanno ciascuno la propria clausola where: class Cache<TKey, TValue> where TKey : notnull where TValue : class.

Un vincolo mancante si manifesta come errore di compilazione nel punto in cui usi il membro, per esempio CS1061 ('T' does not contain a definition for 'CompareTo') oppure CS0304 quando chiami new T() senza new().

default(T)

Il codice generico a volte ha bisogno del "valore vuoto" di un tipo che non conosce. default(T) dà 0 per i numeri, false per bool, null per i tipi riferimento e i tipi nullable, e altrimenti una struct azzerata:

Output:

31
0
Porto
True
0001-01-01

La seconda riga mostra il punto debole di questo schema: per i tipi valore, "non trovato" e "trovato uno 0" sembrano uguali. È per questo che i metodi del framework usano invece la forma TryGet (bool TryGetValue(TKey key, out TValue value)). Da C# 7.1 puoi scrivere il più breve return default; quando il tipo è chiaro dal contesto.

I membri statici sono per tipo costruito

Ogni tipo costruito (Counter<int>, Counter<string>) ha la sua copia dei campi statici della classe:

Output:

2
1
False

A volte è utile (una cache per tipo come TypeInfo<T>.Name calcolata una volta per ogni T) e a volte è una sorpresa quando ti aspettavi un unico contatore condiviso. Metti lo stato condiviso in una classe non generica.

Collezioni generiche

Le collezioni generiche coprono la maggior parte delle esigenze: List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>, e le interfacce che hanno in comune, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T>. Accetta l'interfaccia più ristretta di cui un metodo ha bisogno (IEnumerable<T> se deve solo iterare), e restituisci un tipo concreto o di sola lettura.

Covarianza e controvarianza

Un Dog è un Animal, ma una List<Dog> non è una List<Animal>:

List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'

Se fosse permesso, animals.Add(new Cat()) metterebbe un gatto nella lista dei cani. Le interfacce che producono soltanto valori sono sicure, e vengono dichiarate covarianti con out:

IEnumerable<Animal> readOnly = dogs;          // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs;        // fine: IReadOnlyList<out T>

Il caso speculare, la controvarianza, marcata con in, si applica alle interfacce e ai delegate che consumano soltanto valori: un Action<Animal> si può usare dove è previsto un Action<Dog>, perché qualunque cosa sappia gestire un animale qualsiasi sa gestire un cane. IComparer<in T> è controvariante, e Func<in T, out TResult> usa entrambi i marcatori. La varianza si applica solo a interfacce e delegate, e solo ad argomenti di tipo riferimento: IEnumerable<int> non si converte in IEnumerable<object>.

Errori comuni

  • Usare object dove andrebbe un parametro di tipo. Rinunci ai controlli in compilazione e fai il boxing dei tipi valore.
  • Confrontare due valori T con ==. Senza un vincolo class non compila (CS0019), e con il vincolo confronta i riferimenti. Usa EqualityComparer<T>.Default.Equals(a, b), che funziona per ogni T e rispetta l'Equals del tipo.
  • Restituire default(T) come "non trovato". È ambiguo per i tipi valore. Usa bool TryX(..., out T value).
  • Troppi vincoli. Ognuno restringe chi può usare il metodo. Vincola solo ciò di cui il corpo ha davvero bisogno.
  • Aspettarsi che List<Derived> si converta in List<Base>. Accetta invece IEnumerable<Base> o IReadOnlyList<Base>.

Domande frequenti

Cosa sono i generics in C#?

I generics ti permettono di scrivere una classe, un'interfaccia o un metodo una volta sola con un tipo segnaposto, T, che chi lo usa riempie: List<int>, List<Customer>. Il compilatore controlla ogni uso rispetto al tipo reale, quindi hai la sicurezza dei tipi senza cast, e i tipi valore vengono memorizzati senza boxing. Le collezioni di System.Collections.Generic sono l'esempio più familiare.

Come scrivo un metodo generico in C#?

Metti i parametri di tipo dopo il nome del metodo: static T Largest<T>(List<T> items) where T : IComparable<T> { ... }. Chi chiama di solito omette l'argomento di tipo perché il compilatore lo deduce dagli argomenti: Largest(prices) funziona quando prices è una List<decimal>. Scrivilo esplicitamente, Largest<decimal>(...), quando non c'è niente da cui dedurlo.

Cosa significa where T : class in C#?

È un vincolo: T deve essere un tipo riferimento, quindi il codice generico può assegnare null a un T e usare as T. Altri vincoli sono where T : struct (un tipo valore non nullable), where T : new() (ha un costruttore pubblico senza parametri, quindi il codice può chiamare new T()), where T : SomeBaseClass e where T : ISomeInterface, che permette al codice di chiamare i membri di quell'interfaccia su T.

Cos'è default(T) in C#?

Il valore predefinito del tipo che T si rivela essere: 0 per i numeri, false per bool, null per i tipi riferimento e i tipi nullable, e una struct azzerata per gli altri tipi valore. Il codice generico lo usa quando gli serve "nessun valore" senza conoscere il tipo, per esempio come valore di ritorno di una ricerca fallita. C# 7.1 ha aggiunto il letterale più breve default.

Perché non posso assegnare una List<Dog> a una List<Animal> in C#?

Perché una List<Animal> accetta qualsiasi animale: se l'assegnazione fosse permessa, il codice potrebbe aggiungere un Cat a quella che in realtà è una List<Dog>. Il compilatore la rifiuta (CS0029). Le interfacce di sola lettura sono covarianti, quindi IEnumerable<Animal> animals = dogs; e IReadOnlyList<Animal> animals = dogs; compilano entrambe, perché attraverso di esse non si può aggiungere niente.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA