Classi generiche (Guida per programmatori C#)

Le classi generiche incapsulano operazioni non specifiche di un particolare tipo di dati. Le classi generiche vengono comunemente utilizzate con gli insiemi quali elenchi collegati, tabelle hash, stack, code, strutture ad albero e così via. Le operazioni come l'aggiunta e la rimozione di elementi dall'insieme vengono eseguite fondamentalmente in modo analogo, indipendentemente dal tipo di dati archiviati.

Per la maggior parte degli scenari in cui sono necessarie classi di insiemi, è consigliabile utilizzare quelle fornite nella libreria di classi .NET Framework. Per ulteriori informazioni sull'utilizzo di queste classi, vedere Generics nella libreria di classi .NET Framework (Guida per programmatori C#).

Le classi generiche vengono in genere create a partire da una classe concreta esistente e modificando uno alla volta i tipi nei parametri di tipo fino a raggiungere l'equilibrio ottimale tra generalizzazione e possibilità di utilizzo. Quando si creano classi generiche personalizzate, è importante valutare:

  • Quali sono i tipi da generalizzare in parametri di tipo.

    Di norma, maggiore è il numero di tipi ai quali è possibile applicare parametri, maggiore sarà la flessibilità e la possibilità di riutilizzo del codice. Tuttavia, un'eccessiva generalizzazione può creare un codice che presenta difficoltà di lettura e di comprensione per altri sviluppatori.

  • Quali sono i vincoli, se presenti, da applicare ai parametri di tipo (vedere Vincoli sui parametri di tipo (Guida per programmatori C#)).

    Una buona regola è applicare il massimo numero di vincoli possibili che consentano ancora di gestire i tipi che è necessario gestire. Ad esempio, se la classe generica deve essere utilizzata esclusivamente con tipi riferimento, applicare il vincolo della classe. In questo modo si impedirà l'utilizzo involontario della classe con tipi di valore, sarà possibile utilizzare l'operatore as su T e verificare la presenza di valori null.

  • Se eseguire il factoring del comportamento generico in classi e sottoclassi base.

    Poiché le classi generiche possono essere utilizzate come classi di base, in questo caso vengono applicate le stesse considerazioni di progettazione delle classi non generiche. Vedere le regole sull'ereditarietà dalle classi generiche di base più avanti in questo argomento.

  • Se implementare una o più interfacce generiche.

    Ad esempio, se si sta progettando una classe che verrà utilizzata per creare elementi in un insieme basato su generics, potrebbe essere necessario implementare un'interfaccia come IComparable<T> in cui T rappresenta il tipo della classe.

Per un esempio di classe generica semplice, vedere Introduzione ai generics (Guida per programmatori C#).

Le regole per i vincoli e per i parametri di tipo presentano numerose implicazioni per il comportamento delle classi generiche, soprattutto per quanto riguarda l'ereditarietà e l'accessibilità ai membri. Prima di procedere, è necessario comprendere alcuni termini. Per una classe generica il codice client Node<T>, può fare riferimento alla classe specificando un argomento di tipo, per creare un tipo costruito chiuso (Node<int>) oppure può mantenere il parametro di tipo non specificato, ad esempio quando viene specificata una classe generica di base, per creare un tipo costruito aperto (Node<T>). Le classi generiche possono ereditare da classi base concrete, costruite chiuse o costruite aperte:

class BaseNode { }
class BaseNodeGeneric<T> { }

// concrete type
class NodeConcrete<T> : BaseNode { }

//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }

//open constructed type 
class NodeOpen<T> : BaseNodeGeneric<T> { }

Le classi non generiche, ovvero concrete, possono ereditare da classi di base costruite chiuse, ma non da classi costruite aperte o da parametri di tipo poiché il codice client non è in grado di fornire l'argomento di tipo necessario per creare l'istanza della classe di base in fase di esecuzione.

//No error
class Node1 : BaseNodeGeneric<int> { }

//Generates an error
//class Node2 : BaseNodeGeneric<T> {}

//Generates an error
//class Node3 : T {}

Le classi generiche che ereditano da tipi costruiti aperti devono fornire argomenti di tipo per tutti i parametri di tipo delle classi base non condivisi dalla classe che eredita, come illustrato nel codice riportato di seguito:

class BaseNodeMultiple<T, U> { }

//No error
class Node4<T> : BaseNodeMultiple<T, int> { }

//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {} 

Le classi generiche che ereditano da tipi costruiti aperti devono specificare i vincoli che rappresentano un superset dei vincoli del tipo base o che implicano gli stessi:

class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }

I tipi generici possono utilizzare più vincoli e parametri di tipo, come riportato di seguito:

class SuperKeyType<K, V, U>
    where U : System.IComparable<U>
    where V : new()
{ }

I tipi costruiti aperti o costruiti chiusi possono essere utilizzati come parametri di metodo:

void Swap<T>(List<T> list1, List<T> list2)
{
    //code to swap items
}

void Swap(List<int> list1, List<int> list2)
{
    //code to swap items
}

Se una classe generica implementa un'interfaccia, è possibile eseguire il cast di tutte le istanze della classe a tale interfaccia.

Le classi generiche sono invariabili. In altre parole, se un parametro di input specifica List<BaseClass>, si verificherà un errore in fase di compilazione se si tenta di fornire List<DerivedClass>.

Vedere anche

Riferimenti

Generics (Guida per programmatori C#)

System.Collections.Generic

Concetti

Guida per programmatori C#

Altre risorse

Salvataggio dello stato degli enumeratori

Puzzle relativo all'eredità, parte uno