Panoramica dei tipi generici

Gli sviluppatori usano sempre generics in .NET, in modo implicito o esplicito. Quando si usa LINQ in .NET, forse non si è mai notato l'uso di IEnumerable<T>. Oppure, visualizzando un esempio online di un "repository generico" che comunica con database tramite Entity Framework, si è notato che la maggior parte dei metodi restituisce IQueryable<T>? Probabilmente ci si sarà chiesti che cos'è la T in questi esempi e perché è presente.

Introdotti per la prima volta in .NET Framework 2.0, i generics sono essenzialmente un "modello di codice" che permette agli sviluppatori di definire strutture dei dati indipendenti dai tipi senza il commit in un tipo di dati effettivo. Ad esempio, List<T> è una raccolta generica che può essere dichiarata e usata con qualsiasi tipo, tra cui List<int>, List<string> o List<Person>.

Per dimostrare l'utilità dei generics, verrà esaminata una classe specifica prima e dopo l'aggiunta di generics: ArrayList. In .NET Framework 1.0 gli elementi ArrayList sono di tipo Object. Qualsiasi elemento aggiunto alla raccolta è stato convertito automaticamente in un Object. Lo stesso avviene quando gli elementi vengono letti dall'elenco. Questo processo è noto come conversioni boxing e unboxing e influisce sulle prestazioni. A parte le prestazioni, tuttavia, non c'è modo di determinare il tipo di dati dell'elenco in fase di compilazione, il che rende il codice fragile. I generics risolvono il problema definendo il tipo di dati contenuto da ogni istanza dell'elenco. Ad esempio, è possibile aggiungere numeri interi solo a List<int> e persone solo a List<Person>.

I generics sono disponibili anche al runtime. Il runtime riconosce il tipo di struttura dei dati usato e può archiviarlo in memoria in modo più efficiente.

L'esempio seguente è un piccolo programma che mostra i vantaggi in termini di efficienza ottenuti dalla capacità di riconoscere il tipo di struttura dei dati al runtime:

  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Diagnostics;

  namespace GenericsExample {
    class Program {
      static void Main(string[] args) {
        //generic list
        List<int> ListGeneric = new List<int> { 5, 9, 1, 4 };
        //non-generic list
        ArrayList ListNonGeneric = new ArrayList { 5, 9, 1, 4 };
        // timer for generic list sort
        Stopwatch s = Stopwatch.StartNew();
        ListGeneric.Sort();
        s.Stop();
        Console.WriteLine($"Generic Sort: {ListGeneric}  \n Time taken: {s.Elapsed.TotalMilliseconds}ms");

        //timer for non-generic list sort
        Stopwatch s2 = Stopwatch.StartNew();
        ListNonGeneric.Sort();
        s2.Stop();
        Console.WriteLine($"Non-Generic Sort: {ListNonGeneric}  \n Time taken: {s2.Elapsed.TotalMilliseconds}ms");
        Console.ReadLine();
      }
    }
  }

Questo programma produce un output simile al seguente:

Generic Sort: System.Collections.Generic.List`1[System.Int32]
 Time taken: 0.0034ms
Non-Generic Sort: System.Collections.ArrayList
 Time taken: 0.2592ms

La prima cosa che si può notare è che l'ordinamento dell'elenco generico è notevolmente più veloce rispetto a quello dell'elenco non generico. È anche possibile notare che il tipo per un elenco generico è specifico ([System. Int32]), mentre il tipo per un elenco non generico è generalizzato. Poiché il runtime riconosce che l'oggetto List<int> generico è di tipo Int32, può archiviare in memoria gli elementi dell'elenco in una matrice di numeri interi sottostante, mentre l'oggetto ArrayList non generico deve eseguire il cast di ogni elemento dell'elenco in un oggetto. Come mostra l'esempio, i cast aggiuntivi richiedono tempo e rallentano l'ordinamento dell'elenco.

Un altro vantaggio della capacità del runtime di riconoscere il tipo dell'oggetto generico è una migliore esperienza di debug. Quando si esegue il debug di un tipo generico in C#, è possibile identificare il tipo di ogni elemento nella struttura dei dati. Senza i generics, questo non sarebbe possibile.

Vedi anche