Generics nel runtime (Guida per programmatori C#)
Quando un tipo o un metodo generico viene compilato in Common Intermediate Language (CIL), contiene metadati che lo identificano come contenente parametri di tipo. La modalità d’uso del CIL per un tipo generico varia a seconda che il parametro di tipo fornito sia un tipo valore o un tipo riferimento.
Quando un tipo generico viene costruito per la prima volta con un tipo valore come parametro, il runtime crea un tipo generico specializzato con il parametro o i parametri forniti sostituiti nei percorsi appropriati nel CIL. I tipi generici specializzati vengono creati una sola volta per ogni tipo valore univoco usato come parametro.
Ad esempio, si supponga che il codice programma abbia dichiarato uno stack costituito da interi:
Stack<int>? stack;
A questo punto, il runtime genera una versione specializzata della classe Stack<T> sostituendo l'intero nel modo appropriato per il parametro. D'ora in poi, ogni volta che il codice programma usa uno stack di interi, il runtime riutilizzerà la classe Stack<T> specializzata generata. Nell'esempio seguente vengono create due istanze di uno stack di interi che condividono un'istanza singola nel codice Stack<int>
:
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
Tuttavia, si supponga che venga creata un'altra classe Stack<T> con un tipo valore diverso, ad esempio long
, o una struttura definita dall'utente come parametro in un altro punto del codice. Di conseguenza, il runtime genera un’altra versione del tipo generico e sostituisce long
nei percorsi appropriati nel CIL. Le conversioni non sono più necessarie, perché ogni classe generica specializzata contiene il tipo valore in modo nativo.
I generics hanno un funzionamento leggermente diverso con i tipi riferimento. La prima volta che un tipo generico viene costruito con qualsiasi tipo riferimento, il runtime crea un tipo generico specializzato con riferimenti a oggetti sostituiti per i parametri nel CIL. Quindi, ogni volta che viene creata un'istanza di un tipo costruito con un tipo riferimento come parametro, indipendentemente dal tipo, il runtime riutilizza la versione specializzata precedentemente creata del tipo generico. Questo è possibile perché tutti i riferimenti hanno le stesse dimensioni.
Ad esempio, si supponga di avere due tipi riferimento, una classe Customer
e una classe Order
, e di aver creato uno stack di tipi Customer
:
class Customer { }
class Order { }
Stack<Customer> customers;
A questo punto, il runtime genera una versione specializzata della classe Stack<T> in cui sono archiviati riferimenti a oggetti che verranno compilati successivamente invece di archiviare i dati. Si supponga che la riga di codice successiva crei uno stack di un altro tipo riferimento, chiamato Order
:
Stack<Order> orders = new Stack<Order>();
Diversamente dai tipi valore, non viene creata un'altra versione specializzata della classe Stack<T> per il tipo Order
. Viene invece creata un'istanza della versione specializzata della classe Stack<T> e viene impostata la variabile orders
per referenziarla. Si supponga di individuare una riga di codice per la creazione di uno stack di un tipo Customer
:
customers = new Stack<Customer>();
Come per l'uso precedente della classe Stack<T> creata con il tipo Order
, verrà creata un'altra istanza della classe Stack<T> specializzata. I puntatori contenuti vengono impostati in modo da fare riferimento a un'area di memoria delle dimensioni di un tipo Customer
. Poiché il numero di tipi riferimento può variare ampiamente a seconda del programma, l'implementazione C# di generics riduce notevolmente la quantità di codice limitando a uno il numero di classi specializzate create dal compilatore per classi generiche di tipi riferimento.
Inoltre, quando viene creata un'istanza di una classe C# generica usando un tipo valore o un parametro di tipo riferimento, la reflection può eseguire query sulla classe, accertandone il tipo effettivo e il parametro di tipo.