Condividi tramite


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 tipo deve essere un tipo valore che non ammette i valori Null e che include tipi record struct. 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 vincolo struct implica il vincolo new() e non può essere combinato con il vincolo new(). Non è possibile combinare il vincolo struct con il vincolo unmanaged.
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 T deve 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 tipo classe, interfaccia, delegato o matrice, inclusi i record.
where T : notnull L'argomento di tipo 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 di tipo deve essere un tipo non nullable non gestito. Il vincolo unmanaged implica il vincolo struct e non può essere combinato con i vincoli struct 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 vincolo new() non può essere combinato con i vincoli struct e unmanaged.
where T : <nome della classe di base> L'argomento tipo deve corrispondere alla classe di base specificata o derivare da essa. In un contesto nullable T deve essere un tipo riferimento non nullable derivato dalla classe 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 che ammette i valori Null T può essere un tipo che ammette o non ammette i valori Null 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 T deve 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 che ammette i valori Null T può essere un tipo riferimento che ammette i valori Null, un tipo riferimento che non ammette i valori Null o un tipo valore. T non può essere un tipo valore che ammette i valori Null.
where T : U L'argomento di tipo fornito per T deve essere o derivare dall'argomento fornito per U. In un contesto che ammette i valori Null, se U è un tipo riferimento che non ammette i valori Null, T deve essere un tipo riferimento che non ammette i valori Null. Se U è un tipo riferimento che ammette i valori Null, T può o meno ammettere i valori Null.
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 vincolo default implica il metodo di base senza il vincolo class o struct. Per altre informazioni, vedere la proposta di vincolo default specifica.
where T : allows ref struct Questo anti-vincolo dichiara che l'argomento di tipo per T può essere un tipo ref struct. Il tipo o il metodo generico deve rispettare le regole di sicurezza di riferimento per qualsiasi istanza di T poiché potrebbe trattarsi di ref struct.

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

  • È possibile applicare al massimo uno tra i vincoli struct, class, class?, notnull e unmanaged. Se si specifica uno di questi vincoli, deve essere il primo vincolo specificato per tale parametro di tipo.
  • Il vincolo della classe di base, (where T : Base o where T : Base?), non può essere combinato con nessuno dei vincoli struct, class, class?, notnull o unmanaged.
  • È possibile applicare al massimo un vincolo della classe di base, in entrambi i moduli. Se si vuole supportare il tipo di base che ammette i valori Null, usare Base?.
  • Non è possibile assegnare un nome di costante sia alla forma di un'interfaccia che ammette i valori Null che a quella che li ammette.
  • Il vincolo new() non può essere combinato con il vincolo struct o unmanaged. Se si specifica il vincolo new(), deve essere l'ultimo vincolo per tale parametro di tipo. Gli anti-vincoli, se applicabili, possono seguire il vincolo new().
  • Il vincolo default può essere applicato solo alle implementazioni esplicite o di override dell'interfaccia. Non può essere combinato con i vincoli struct o class.
  • Il vincolo allows ref struct non può essere combinato con il vincolo class o class?.
  • L'anti-vincolo allows ref struct deve seguire tutti i vincoli per tale parametro di tipo.

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. Specificando il vincolo della classe di base, ad esempio, si indica al compilatore che solo gli oggetti del tipo specificato o derivati da tale tipo possono sostituire questo argomento 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 : notnull, Employee, IComparable<T>, new()
{
    // ...
    public void AddDefault()
    {
        T t = new T();
        // ...
    }
}

Quando si applica il vincolo where T : class, evitare gli operatori == e != nel parametro di tipo perché questi operatori verificano solo l'identità del riferimento e non 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 quindi deve usare gli operatori predefiniti validi per tutti i tipi riferimento. Per verificare l'uguaglianza dei valori, applicare il vincolo where T : IEquatable<T> 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:

  • Non è possibile usare gli operatori != e == perché non viene garantito che siano supportati dall'argomento tipo concreto.
  • Possono essere convertiti in e da System.Object oppure convertiti in modo esplicito in qualsiasi tipo di interfaccia.
  • È possibile confrontarli con Null. Se si confronta un parametro senza vincoli con null e l'argomento tipo è un tipo valore, viene sempre restituito false.

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.

vincolonotnull

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

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

vincoloclass

Il vincolo class 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.

vincolodefault

L'aggiunta di tipi riferimento nullable complica l'uso di T? in un tipo o metodo generico. T? può essere usato con il vincolo struct o class, ma uno di essi deve essere presente. Quando è stato usato il vincolo class, T? ha 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 a T. In altre parole, non diventa T??.

Poiché T? può ora essere usato senza il vincolo class 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 il vincolo class o struct, le classi derivate devono in qualche modo specificare un override che si applica al metodo di base senza alcun vincolo. Il metodo derivato applica il vincolo default. Il vincolo di default non chiarisce il vincolo classstruct.

Vincolo non gestito

È possibile usare il vincolo unmanaged 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 vincolo unmanaged implica il vincolo struct e non può essere combinato con esso. Poiché il vincolo struct implica il vincolo new(), il vincolo unmanaged non può essere combinato anche con il vincolo new().

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;

È possibile usare il metodo precedente per combinare delegati dello stesso tipo:

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. first e test sono entrambi tipi delegati, ma sono tipi delegati diversi.

Vincoli di enumerazione

È anche possibile specificare il tipo System.Enum come vincolo di classe di 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 usano la reflection, che ha implicazioni sulle prestazioni. È possibile chiamare EnumNamedValues per creare una raccolta da memorizzare nella cache e riusare anziché ripetere le chiamate che richiedono 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>
{
    static abstract T operator +(T left, T right);
    static abstract T operator -(T left, T right);
}

Questo modello consente al compilatore C# di determinare il tipo contenitore per gli operatori di overload o qualsiasi metodo static virtual 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>
{
    static abstract IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    static abstract 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.

Consente lo struct di riferimento

L'anti-vincolo allows ref struct dichiara che l'argomento di tipo corrispondente può essere un tipo ref struct. Le istanze del parametro di tipo devono rispettare le regole seguenti:

  • Non può essere boxed.
  • Partecipa a regole di sicurezza di riferimento.
  • Non è possibile usare istanze in cui non è consentito un tipo di ref struct, ad esempio campi static.
  • Le istanze possono essere contrassegnate con il modificatore scoped.

La clausola allows ref struct non viene ereditata. Nel codice seguente:

class SomeClass<T, S>
    where T : allows ref struct
    where S : T
{
    // etc
}

L'argomento per S non può essere ref struct perché S non ha la clausola allows ref struct.

Un parametro di tipo con la clausola allows ref struct non può essere usato come argomento di tipo, a meno che il parametro di tipo corrispondente non abbia anche la clausola allows ref struct. Questa regola è illustrata nell'esempio seguente:

public class Allow<T> where T : allows ref struct
{

}

public class Disallow<T>
{
}

public class Example<T> where T : allows ref struct
{
    private Allow<T> fieldOne; // Allowed. T is allowed to be a ref struct

    private Disallow<T> fieldTwo; // Error. T is not allowed to be a ref struct
}

L'esempio precedente mostra che un argomento di tipo che potrebbe essere un tipo ref struct non può essere sostituito con un parametro di tipo che non può essere un tipo ref struct.

Vedi anche