Panoramica dei tipi generici

Gli sviluppatori usano generics sempre in .NET, sia in modo implicito che in modo esplicito. Ti sei mai accorto che stai lavorando con IEnumerable<T> quando usi LINQ in .NET? In alternativa, se è stato visualizzato un esempio online di un "repository generico" per comunicare con i database usando Entity Framework, si è visto che la maggior parte dei metodi restituisce IQueryable<T>? Ci si potrebbe chiedere che cos'è il T in questi esempi e perché è lì dentro.

I generics sono essenzialmente un "modello di codice" che consente agli sviluppatori di definire strutture di dati indipendenti dai tipi senza eseguire il commit in un tipo di dati effettivo. Ad esempio, List<T> è una raccolta generica che può essere dichiarata e usata con qualsiasi tipo, ad esempio List<int>, List<string>o List<Person>.

Per comprendere perché i generics sono utili, si esaminerà una classe specifica prima e dopo l'aggiunta di generics: ArrayList. In .NET Framework 1.0 gli ArrayList elementi erano di tipo Object. Qualsiasi elemento aggiunto alla raccolta è stato convertito automaticamente in un oggetto Object. Lo stesso avviene durante la lettura degli elementi dall'elenco. Questo processo, noto come boxing e unboxing, influisce sulle prestazioni. A parte le prestazioni, tuttavia, non c'è modo di determinare il tipo di dati nell'elenco in fase di compilazione, che rende un codice fragile. I generics risolvono questo problema definendo il tipo di dati che ogni istanza dell'elenco conterrà. Ad esempio, è possibile aggiungere solo numeri interi a List<int> e aggiungere solo persone a List<Person>.

I generics sono disponibili anche in fase di esecuzione. Il runtime conosce il tipo di struttura dei dati in uso e può archiviarlo in memoria in modo più efficiente.

L'esempio seguente è un piccolo programma che illustra l'efficienza della conoscenza del tipo di struttura di dati in fase di esecuzione:

  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 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 qui è che l'ordinamento dell'elenco generico è notevolmente più veloce rispetto all'ordinamento dell'elenco non generico. È anche possibile notare che il tipo per l'elenco generico è distinto ([System.Int32]), mentre il tipo per l'elenco non generico è generalizzato. Poiché il runtime sa che il generico List<int> è di tipo Int32, può archiviare gli elementi dell'elenco in una matrice integer sottostante in memoria, mentre il non generico ArrayList deve eseguire il cast di ogni elemento dell'elenco a un oggetto . Come illustrato in questo esempio, i cast aggiuntivi richiedono tempo e rallentano l'ordinamento dell'elenco.

Un ulteriore vantaggio del runtime che conosce il tipo del tuo generico è una migliore esperienza di debug. Quando si esegue il debug di un generico in C#, si conosce il tipo di ogni elemento nella struttura dei dati. Senza generics, non avresti idea del tipo che ogni elemento era.

Vedere anche