Vincoli sui parametri di tipo (Guida per programmatori C#)

I vincoli indicano al compilatore quali funzionalità deve usare un argomento tipo. Senza i vincoli, l'argomento tipo può essere qualsiasi tipo. Il compilatore è in grado di dedurre solo i membri di System.Object, che è la principale classe di base per qualsiasi tipo .NET. Per altre informazioni, vedere Motivi per cui usare i vincoli. Se il codice client usa un tipo che non soddisfa un vincolo, il compilatore genera un errore. I vincoli vengono specificati usando la parola chiave contestuale where. Nella tabella seguente sono elencati i vari tipi di vincoli:

Vincolo Descrizione
where T : struct L'argomento type deve essere un tipo valore non nullable, che include record struct tipi. Per informazioni sui tipi valore nullable, vedere Tipi valore nullable. Poiché tutti i tipi valore hanno un costruttore senza parametri accessibile, dichiarato o implicito, il struct vincolo implica il new() vincolo e non può essere combinato con il new() vincolo. Non è possibile combinare il struct vincolo con il unmanaged vincolo .
where T : class L'argomento tipo deve essere un tipo riferimento. Questo vincolo si applica anche a qualsiasi tipo di classe, interfaccia, delegato o matrice. In un contesto nullable deve T essere un tipo riferimento non nullable.
where T : class? L'argomento di tipo deve essere un tipo riferimento, nullable o non nullable. Questo vincolo si applica anche a qualsiasi classe, interfaccia, delegato o tipo di matrice, inclusi i record.
where T : notnull L'argomento type deve essere un tipo non nullable. L'argomento può essere un tipo riferimento non nullable o un tipo valore non nullable.
where T : unmanaged L'argomento type deve essere un tipo non nullable non gestito. Il unmanaged vincolo implica il struct vincolo e non può essere combinato con i struct vincoli o new() .
where T : new() L'argomento tipo deve avere un costruttore pubblico senza parametri. Quando il vincolo new() viene usato con altri vincoli, deve essere specificato per ultimo. Il new() vincolo non può essere combinato con i struct vincoli e unmanaged .
where T :<nome della classe base> L'argomento tipo deve corrispondere alla classe di base specificata o derivare da essa. In un contesto nullable deve T essere un tipo riferimento non nullable derivato dalla classe di base specificata.
where T :<nome> della classe di base? L'argomento tipo deve corrispondere alla classe di base specificata o derivare da essa. In un contesto T nullable può essere un tipo nullable o non nullable derivato dalla classe di base specificata.
where T :<nome dell'interfaccia> L'argomento tipo deve corrispondere all'interfaccia specificata o implementare tale interfaccia. È possibile specificare più vincoli di interfaccia. L'interfaccia vincolante può anche essere generica. In un contesto nullable deve T essere un tipo non nullable che implementa l'interfaccia specificata.
where T :<nome> dell'interfaccia? L'argomento tipo deve corrispondere all'interfaccia specificata o implementare tale interfaccia. È possibile specificare più vincoli di interfaccia. L'interfaccia vincolante può anche essere generica. In un contesto T nullable può essere un tipo riferimento nullable, un tipo riferimento non nullable o un tipo valore. T non può essere un tipo di valore nullable.
where T : U L'argomento di tipo fornito per T deve essere o derivare dall'argomento fornito per U. In un contesto nullable, se U è un tipo riferimento non nullable, T deve essere un tipo riferimento non nullable. Se U è un tipo riferimento nullable, T può essere nullable o non nullable.
where T : default Questo vincolo risolve l'ambiguità quando è necessario specificare un parametro di tipo non vincolato quando si esegue l'override di un metodo o si fornisce un'implementazione esplicita dell'interfaccia. Il default vincolo implica il metodo di base senza il class vincolo o struct . Per altre informazioni, vedere la proposta relativa alle specifiche di default vincolo .

Alcuni vincoli si escludono a vicenda e alcuni vincoli devono essere in un ordine specificato:

  • È possibile applicare al massimo uno dei structvincoli , class, class?, notnulle unmanaged . Se si specifica uno di questi vincoli, deve essere il primo vincolo specificato per tale parametro di tipo.
  • Il vincolo della classe base (where T : Base o where T : Base?) non può essere combinato con nessuno dei vincoli struct, , classclass?, notnullo unmanaged.
  • È possibile applicare al massimo un vincolo di classe base, in entrambi i moduli. Se si vuole supportare il tipo di base nullable, usare Base?.
  • Non è possibile assegnare un nome sia alla forma non nullable che nullable di un'interfaccia come vincolo.
  • Il vincolo new() non può essere combinato con il vincolo struct o unmanaged. Se si specifica il new() vincolo, deve essere l'ultimo vincolo per il parametro di tipo.
  • Il default vincolo può essere applicato solo alle implementazioni dell'interfaccia di override o esplicite. Non può essere combinato con i struct vincoli o class .

Motivi per cui usare i vincoli

I vincoli specificano le funzionalità e le aspettative di un parametro di tipo. La dichiarazione di tali vincoli significa che è possibile usare le operazioni e le chiamate al metodo del tipo di vincolo. Si applicano vincoli al parametro di tipo quando la classe o il metodo generico usa qualsiasi operazione sui membri generici oltre all'assegnazione semplice, che include la chiamata di qualsiasi metodo non supportato da System.Object. Ad esempio, il vincolo della classe base indica al compilatore che solo gli oggetti di questo tipo o derivati da questo tipo possono sostituire tale argomento di tipo. In presenza di questa garanzia, il compilatore può consentire le chiamate ai metodi del tipo all'interno della classe generica. L'esempio di codice seguente illustra la funzionalità che è possibile aggiungere alla classe GenericList<T> (in Introduzione ai generics) applicando un vincolo della classe di base.

public class Employee
{
    public Employee(string name, int id) => (Name, ID) = (name, id);
    public string Name { get; set; }
    public int ID { get; set; }
}

public class GenericList<T> where T : Employee
{
    private class Node
    {
        public Node(T t) => (Next, Data) = (null, t);

        public Node? Next { get; set; }
        public T Data { get; set; }
    }

    private Node? head;

    public void AddHead(T t)
    {
        Node n = new Node(t) { Next = head };
        head = n;
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node? current = head;

        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    public T? FindFirstOccurrence(string s)
    {
        Node? current = head;
        T? t = null;

        while (current != null)
        {
            //The constraint enables access to the Name property.
            if (current.Data.Name == s)
            {
                t = current.Data;
                break;
            }
            else
            {
                current = current.Next;
            }
        }
        return t;
    }
}

Il vincolo consente alla classe generica di usare la proprietà Employee.Name. Il vincolo specifica che tutti gli elementi di tipo T sono sicuramente un oggetto Employee o un oggetto che eredita da Employee.

È possibile applicare più vincoli allo stesso parametro di tipo. I vincoli stessi possono essere tipi generici, come illustrato di seguito:

class EmployeeList<T> where T : Employee, System.Collections.Generic.IList<T>, IDisposable, new()
{
    // ...
}

Quando si applica il where T : class vincolo, evitare gli == operatori e != nel parametro di tipo perché questi operatori testano solo l'identità di riferimento, non per l'uguaglianza dei valori. Questo comportamento si verifica anche se si esegue l'overload degli operatori in un tipo usato come argomento. Il codice seguente illustra questo aspetto. L'output è false anche se la classe String esegue l'overload dell'operatore ==.

public static void OpEqualsTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s == t);
}

private static void TestStringEquality()
{
    string s1 = "target";
    System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
    string s2 = sb.ToString();
    OpEqualsTest<string>(s1, s2);
}

Il compilatore sa solo che T è un tipo riferimento in fase di compilazione e deve usare gli operatori predefiniti validi per tutti i tipi di riferimento. Se è necessario verificare l'uguaglianza dei valori, applicare il where T : IEquatable<T> vincolo o where T : IComparable<T> e implementare l'interfaccia in qualsiasi classe usata per costruire la classe generica.

Vincolo di più parametri

È possibile applicare vincoli a più parametri e più vincoli a un singolo parametro, come illustrato nell'esempio seguente:

class Base { }
class Test<T, U>
    where U : struct
    where T : Base, new()
{ }

Parametri di tipo senza vincoli

I parametri di tipo che non hanno vincoli, ad esempio T nella classe pubblica SampleClass<T>{}, sono detti parametri di tipo senza vincoli. I parametri di tipo senza vincoli prevedono le regole seguenti:

  • Gli != operatori e == non possono essere usati perché non esiste alcuna garanzia che l'argomento del tipo concreto supporti questi operatori.
  • Possono essere convertiti in e da System.Object oppure convertiti in modo esplicito in qualsiasi tipo di interfaccia.
  • È possibile confrontarli con Null. Se un parametro non associato viene confrontato con null, il confronto restituisce sempre false se l'argomento di tipo è un tipo valore.

Parametri di tipo come vincoli

L'uso di un parametro di tipo generico come vincolo è utile quando una funzione membro con il proprio parametro di tipo deve vincolare tale parametro a quello del tipo che lo contiene, come illustrato nell'esempio seguente:

public class List<T>
{
    public void Add<U>(List<U> items) where U : T {/*...*/}
}

Nell'esempio precedente T è un vincolo di tipo nel contesto del metodo Add e un parametro di tipo senza vincoli nel contesto della classe List.

I parametri di tipo possono anche essere usati come vincoli nelle definizioni di classi generiche. Il parametro di tipo deve essere dichiarato tra parentesi acute, insieme a eventuali altri parametri di tipo:

//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }

L'utilità dei parametri di tipo usati come vincoli in classi generiche è limitata poiché il compilatore non può presupporre niente riguardo al parametro di tipo, tranne il fatto che deriva da System.Object. Usare i parametri di tipo come vincoli nelle classi generiche in scenari in cui si vuole applicare una relazione di ereditarietà tra due parametri di tipo.

notnull Vincolo

È possibile utilizzare il notnull vincolo per specificare che l'argomento di tipo deve essere un tipo di valore non nullable o un tipo riferimento non nullable. A differenza della maggior parte degli altri vincoli, se un argomento di tipo viola il notnull vincolo, il compilatore genera un avviso anziché un errore.

Il notnull vincolo ha un effetto solo quando viene usato in un contesto nullable. Se si aggiunge il notnull vincolo in un contesto oblivious nullable, il compilatore non genera avvisi o errori per violazioni del vincolo.

class Vincolo

Il class vincolo in un contesto nullable specifica che l'argomento di tipo deve essere un tipo riferimento non nullable. In un contesto nullable, quando un argomento di tipo è un tipo riferimento nullable, il compilatore genera un avviso.

default Vincolo

L'aggiunta di tipi riferimento nullable complica l'uso di T? in un tipo o metodo generico. T? può essere usato con il struct vincolo o class , ma uno di essi deve essere presente. Quando è stato usato il class vincolo, T? viene fatto riferimento al tipo riferimento nullable per T. T? può essere usato quando non viene applicato alcun vincolo. In tal caso, T? viene interpretato come T? per i tipi valore e i tipi riferimento. Tuttavia, se T è un'istanza di Nullable<T>, T? è uguale Ta . In altre parole, non diventa T??.

Poiché T? ora è possibile usare senza il class vincolo o struct , le ambiguità possono verificarsi in override o implementazioni esplicite dell'interfaccia. In entrambi i casi, l'override non include i vincoli, ma li eredita dalla classe di base. Quando la classe base non applica né il class vincolo o struct , le classi derivate devono in qualche modo specificare un override si applica al metodo di base senza alcun vincolo. Il metodo derivato applica il default vincolo . Il default vincolo non chiarisce il class vincolo né struct .

Vincolo non gestito

È possibile usare il unmanaged vincolo per specificare che il parametro di tipo deve essere un tipo non nullable non gestito. Il vincolo unmanaged consente di scrivere routine riutilizzabili per lavorare con tipi che possono essere modificati come blocchi di memoria, come illustrato nell'esempio seguente:

unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
    var size = sizeof(T);
    var result = new Byte[size];
    Byte* p = (byte*)&argument;
    for (var i = 0; i < size; i++)
        result[i] = *p++;
    return result;
}

Il metodo precedente deve essere compilato in un contesto unsafe perché usa l'operatore sizeof per un tipo non noto come tipo predefinito. Senza il vincolo unmanaged l'operatore sizeof non è disponibile.

Il unmanaged vincolo implica il struct vincolo e non può essere combinato con esso. Poiché il struct vincolo implica il new() vincolo, il unmanaged vincolo non può essere combinato anche con il new() vincolo.

Vincoli dei delegati

È possibile usare System.Delegate o System.MulticastDelegate come vincolo di classe di base. Il supporto Common Language Runtime (CLR) consente sempre questo vincolo, a differenza del linguaggio C#. Il vincolo System.Delegate consente di scrivere codice che funziona con i delegati in modo indipendente dai tipi. Il codice seguente definisce un metodo di estensione che combina due delegati purché siano dello stesso tipo:

public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
    where TDelegate : System.Delegate
    => Delegate.Combine(source, target) as TDelegate;

Per combinare delegati dello stesso tipo, è possibile usare il metodo riportato sopra:

Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");

var combined = first.TypeSafeCombine(second);
combined!();

Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);

Se si rimuove il commento dall'ultima riga, non verrà compilata. Sia first che test sono tipi delegati, ma sono tipi delegati diversi.

Vincoli di enumerazione

È anche possibile specificare il System.Enum tipo come vincolo di classe base. Il supporto Common Language Runtime (CLR) consente sempre questo vincolo, a differenza del linguaggio C#. I generics che usano System.Enum offrono una programmazione indipendente dai tipi che consente di memorizzare nella cache i risultati dei metodi statici in System.Enum. Nell'esempio seguente vengono individuati tutti i valori validi per un tipo di enumerazione e viene compilato un dizionario che esegue il mapping di tali valori alla propria rappresentazione di stringa.

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    var values = Enum.GetValues(typeof(T));

    foreach (int item in values)
        result.Add(item, Enum.GetName(typeof(T), item)!);
    return result;
}

Enum.GetValues e Enum.GetName usare la reflection, che ha implicazioni sulle prestazioni. È possibile chiamare EnumNamedValues per compilare una raccolta memorizzata nella cache e riutilizzata anziché ripetere le chiamate che richiedono la reflection.

Il metodo può essere usato come illustrato nell'esempio seguente per creare un'enumerazione e compilare un dizionario dei relativi valori e nomi:

enum Rainbow
{
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet
}
var map = EnumNamedValues<Rainbow>();

foreach (var pair in map)
    Console.WriteLine($"{pair.Key}:\t{pair.Value}");

Gli argomenti di tipo implementano l'interfaccia dichiarata

Alcuni scenari richiedono che un argomento fornito per un parametro di tipo implementi tale interfaccia. Ad esempio:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static T operator +(T left, T right);
    public abstract static T operator -(T left, T right);
}

Questo modello consente al compilatore C# di determinare il tipo contenitore per gli operatori di overload o qualsiasi static virtual metodo o static abstract . Fornisce la sintassi in modo che gli operatori di addizione e sottrazione possano essere definiti in un tipo contenitore. Senza questo vincolo, i parametri e gli argomenti devono essere dichiarati come interfaccia, anziché come parametro di tipo:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    public abstract static IAdditionSubtraction<T> operator -(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);
}

La sintassi precedente richiederebbe agli implementatori di usare l'implementazione esplicita dell'interfaccia per tali metodi. Se si specifica il vincolo aggiuntivo, l'interfaccia consente di definire gli operatori in termini di parametri di tipo. I tipi che implementano l'interfaccia possono implementare in modo implicito i metodi di interfaccia.

Vedi anche