Begränsningar för typparametrar (C#-programmeringsguide)

Begränsningar informerar kompilatorn om de funktioner som ett typargument måste ha. Utan några begränsningar kan typargumentet vara vilken typ som helst. Kompilatorn kan bara anta medlemmarna System.Objecti , vilket är den ultimata basklassen för alla .NET-typer. Mer information finns i Varför använda begränsningar. Om klientkoden använder en typ som inte uppfyller en begränsning utfärdar kompilatorn ett fel. Begränsningar anges med hjälp av det kontextuella nyckelordet where . I följande tabell visas de olika typerna av begränsningar:

Begränsning beskrivning
where T : struct Typargumentet måste vara en värdetyp som inte kan nullvärde, som innehåller record struct typer. Information om typer av null-värden finns i Nullable value types (Nullable value types). Eftersom alla värdetyper har en tillgänglig parameterlös konstruktor, antingen deklarerad eller implicit, innebär villkoret struct villkoret new() och kan inte kombineras med villkoret new() . Du kan inte kombinera villkoret struct med villkoret unmanaged .
where T : class Typargumentet måste vara en referenstyp. Den här begränsningen gäller även för alla typer av klasser, gränssnitt, ombud eller matriser. I en nullbar kontext T måste den vara en referenstyp som inte kan nulleras.
where T : class? Typargumentet måste vara en referenstyp, antingen nullbar eller icke-nullbar. Den här begränsningen gäller även för alla klasser, gränssnitt, ombud eller matristyper, inklusive poster.
where T : notnull Typargumentet måste vara en icke-nullbar typ. Argumentet kan vara en icke-nullbar referenstyp eller en värdetyp som inte kan ogiltigförklaras.
where T : unmanaged Typargumentet måste vara en icke-nullbar ohanterad typ. Villkoret unmanaged innebär villkoret struct och kan inte kombineras med antingen begränsningarna struct eller new() .
where T : new() Typargumentet måste ha en offentlig parameterlös konstruktor. När villkoret används tillsammans med andra begränsningar måste det new() anges sist. Villkoret new() kan inte kombineras med begränsningarna struct och unmanaged .
where T :<basklassnamn> Typargumentet måste vara eller härleda från den angivna basklassen. I ett null-sammanhang T måste det vara en referenstyp som inte kan nollföras från den angivna basklassen.
where T :<basklassnamn>? Typargumentet måste vara eller härleda från den angivna basklassen. I en nullbar kontext T kan vara antingen en nullbar eller icke-nullbar typ som härletts från den angivna basklassen.
where T :<gränssnittsnamn> Typargumentet måste vara eller implementera det angivna gränssnittet. Du kan ange flera gränssnittsbegränsningar. Det begränsande gränssnittet kan också vara allmänt. I en null-kontext T måste vara en icke-nullbar typ som implementerar det angivna gränssnittet.
where T :<gränssnittsnamn>? Typargumentet måste vara eller implementera det angivna gränssnittet. Du kan ange flera gränssnittsbegränsningar. Det begränsande gränssnittet kan också vara allmänt. I en nullbar kontext T kan det vara en referenstyp som kan ogiltigförklaras, en referenstyp som inte kan nollföras eller en värdetyp. T kan inte vara en nullbar värdetyp.
where T : U Typargumentet som anges för T måste vara eller härledas från argumentet som angetts för U. Om det är en referenstyp T som inte kan nollföras måste den i ett null-sammanhang U vara en referenstyp som inte kan null- Om U är en nullbar referenstyp T kan vara antingen nullbar eller icke-nullbar.
where T : default Den här begränsningen löser tvetydigheten när du behöver ange en icke-tränad typparameter när du åsidosätter en metod eller tillhandahåller en explicit gränssnittsimplementering. Villkoret default innebär att basmetoden saknar villkoret class eller struct . Mer information finns i villkorsspecifikationsförslagetdefault.

Vissa begränsningar är ömsesidigt uteslutande och vissa begränsningar måste vara i en angiven ordning:

  • Du kan använda högst en av begränsningarna struct, class, class?, notnulloch unmanaged . Om du anger någon av dessa begränsningar måste det vara den första begränsningen som anges för den typparametern.
  • Basklassvillkoret (where T : Base eller where T : Base?), kan inte kombineras med någon av begränsningarna struct, class, class?, notnulleller unmanaged.
  • Du kan använda högst en basklassbegränsning i båda formulären. Om du vill stödja den nullbara bastypen använder du Base?.
  • Du kan inte namnge både den icke-nullbara och nullbara formen för ett gränssnitt som en begränsning.
  • Villkoret new() kan inte kombineras med villkoret struct eller unmanaged . Om du anger villkoret new() måste det vara den sista begränsningen för den typparametern.
  • Begränsningen default kan endast tillämpas på åsidosättning eller explicita gränssnittsimplementeringar. Det kan inte kombineras med begränsningarna struct eller class .

Varför använda begränsningar

Begränsningar anger funktioner och förväntningar för en typparameter. Om du deklarerar dessa begränsningar kan du använda åtgärderna och metodanropen av den begränsande typen. Du tillämpar begränsningar på typparametern när din generiska klass eller metod använder någon åtgärd på de generiska medlemmarna utöver enkel tilldelning, vilket inkluderar att anropa alla metoder som inte stöds av System.Object. Basklassvillkoret anger till exempel för kompilatorn att endast objekt av den här typen eller härledda från den här typen kan ersätta argumentet av den typen. När kompilatorn har den här garantin kan den tillåta att metoder av den typen anropas i den generiska klassen. I följande kodexempel visas de funktioner som du kan lägga till i GenericList<T> klassen (i Introduktion till generiska objekt) genom att tillämpa en basklassbegränsning.

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;
    }
}

Villkoret gör det möjligt för den generiska klassen att använda Employee.Name egenskapen. Villkoret anger att alla objekt av typen T garanteras vara antingen ett Employee objekt eller ett objekt som ärver från Employee.

Flera begränsningar kan tillämpas på samma typparameter och själva begränsningarna kan vara generiska typer, enligt följande:

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

När du tillämpar villkoret where T : class bör du undvika operatorerna == och != för typparametern eftersom dessa operatorer endast testar referensidentiteten, inte för värdejämlikhet. Det här beteendet inträffar även om dessa operatorer är överbelastade i en typ som används som argument. Följande kod illustrerar den här punkten. utdata är false trots att String klassen överbelastar operatorn == .

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);
}

Kompilatorn vet bara att det T är en referenstyp vid kompileringstid och måste använda de standardoperatorer som är giltiga för alla referenstyper. Om du måste testa för värdejämlikhet tillämpar du villkoret where T : IEquatable<T> eller where T : IComparable<T> och implementerar gränssnittet i alla klasser som används för att konstruera den generiska klassen.

Begränsa flera parametrar

Du kan tillämpa begränsningar på flera parametrar och flera begränsningar för en enskild parameter, som du ser i följande exempel:

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

Parametrar för obundna typer

Typparametrar som inte har några begränsningar, till exempel T i den offentliga klassen SampleClass<T>{}, kallas för obundna typparametrar. Parametrar för obundna typer har följande regler:

  • Operatorerna != och == kan inte användas eftersom det inte finns någon garanti för att argumentet för konkret typ stöder dessa operatorer.
  • De kan konverteras till och från System.Object eller explicit konverteras till vilken gränssnittstyp som helst.
  • Du kan jämföra dem med null. Om en obundna parameter jämförs med nullreturnerar jämförelsen alltid false om typargumentet är en värdetyp.

Ange parametrar som begränsningar

Användningen av en allmän typparameter som en begränsning är användbar när en medlemsfunktion med en egen typparameter måste begränsa parametern till typparametern för den innehållande typen, som du ser i följande exempel:

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

I föregående exempel T är en typbegränsning i kontexten Add för metoden och en obundna typparameter i kontexten för List klassen.

Typparametrar kan också användas som begränsningar i allmänna klassdefinitioner. Typparametern måste deklareras inom vinkelparenteserna tillsammans med andra typparametrar:

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

Användbarheten för typparametrar som begränsningar med generiska klasser är begränsad eftersom kompilatorn inte kan anta något om typparametern förutom att den härleds från System.Object. Använd typparametrar som begränsningar för allmänna klasser i scenarier där du vill framtvinga en arvsrelation mellan två typparametrar.

notnull Begränsning

Du kan använda villkoret notnull för att ange att typargumentet måste vara en icke-nullbar värdetyp eller icke-nullbar referenstyp. Till skillnad från de flesta andra begränsningar genererar kompilatorn en varning i stället för ett fel om ett typargument bryter mot villkoret notnull .

Villkoret notnull har endast effekt när det används i en nullbar kontext. Om du lägger till villkoret notnull i en nullbar omedveten kontext genererar kompilatorn inga varningar eller fel för överträdelser av villkoret.

class Begränsning

Villkoret class i en nullbar kontext anger att typargumentet måste vara en referenstyp som inte kan nulleras. När ett typargument är en nullbar referenstyp i en nullbar kontext genererar kompilatorn en varning.

default Begränsning

Tillägget av nullbara referenstyper komplicerar användningen av T? i en allmän typ eller metod. T? kan användas med antingen villkoret struct eller class , men en av dem måste finnas. När villkoret class användes T? hänvisade du till referenstypen Tnull för . T? kan användas när inget av begränsningarna tillämpas. I så fall T? tolkas som T? för värdetyper och referenstyper. Men om T är en instans av Nullable<T>, T? är samma som T. Med andra ord blir T??det inte .

Eftersom T? nu kan användas utan begränsningen class eller struct kan tvetydigheter uppstå i åsidosättningar eller explicita gränssnittsimplementeringar. I båda dessa fall inkluderar åsidosättningen inte begränsningarna, utan ärver dem från basklassen. När basklassen inte tillämpar villkoret class eller struct måste härledda klasser på något sätt ange en åsidosättning som gäller för basmetoden utan någon av begränsningarna. Den härledda metoden tillämpar villkoret default . Villkoret klargör varken villkoret class eller struct .default

Ohanterad begränsning

Du kan använda villkoret unmanaged för att ange att typparametern måste vara en icke-nullbar ohanterad typ. Med villkoret unmanaged kan du skriva återanvändbara rutiner för att arbeta med typer som kan manipuleras som minnesblock, som du ser i följande exempel:

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;
}

Föregående metod måste kompileras i en unsafe kontext eftersom den använder operatorn sizeof på en typ som inte är känd för att vara en inbyggd typ. Utan begränsningen unmanaged är operatorn sizeof inte tillgänglig.

Villkoret unmanaged innebär villkoret struct och kan inte kombineras med det. Eftersom villkoret struct innebär villkoret new() kan villkoret unmanaged inte kombineras med villkoret new() också.

Delegera begränsningar

Du kan använda System.Delegate eller System.MulticastDelegate som en basklassbegränsning. CLR tillät alltid den här begränsningen, men C#-språket tillät det inte. Med villkoret System.Delegate kan du skriva kod som fungerar med ombud på ett typsäkert sätt. Följande kod definierar en tilläggsmetod som kombinerar två ombud förutsatt att de är av samma typ:

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

Du kan använda metoden ovan för att kombinera ombud som är av samma typ:

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);

Om du avkommentarer den sista raden kompileras den inte. Både first och test är ombudstyper, men de är olika ombudstyper.

Uppräkningsbegränsningar

Du kan också ange System.Enum typen som en basklassbegränsning. CLR tillät alltid den här begränsningen, men C#-språket tillät det inte. Generiska program som använder System.Enum ger typsäker programmering för att cachelagra resultat från att använda statiska metoder i System.Enum. Följande exempel hittar alla giltiga värden för en uppräkningstyp och skapar sedan en ordlista som mappar dessa värden till dess strängrepresentation.

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 och Enum.GetName använda reflektion, vilket har prestandakonsekvenser. Du kan anropa EnumNamedValues för att skapa en samling som cachelagras och återanvänds i stället för att upprepa de anrop som kräver reflektion.

Du kan använda den enligt följande exempel för att skapa en uppräkning och skapa en ordlista med dess värden och namn:

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}");

Typargument implementerar deklarerat gränssnitt

Vissa scenarier kräver att ett argument som anges för en typparameter implementerar gränssnittet. Till exempel:

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);
}

Det här mönstret gör det möjligt för C#-kompilatorn att fastställa den innehållande typen för de överbelastade operatorerna, eller någon eller static virtualstatic abstract -metoden. Den innehåller syntaxen så att operatorerna addition och subtraktion kan definieras för en innehållande typ. Utan den här begränsningen skulle parametrarna och argumenten behöva deklareras som gränssnitt i stället för typparametern:

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);
}

Den föregående syntaxen skulle kräva att implementerare använder explicit gränssnittsimplementering för dessa metoder. Om du anger den extra begränsningen kan gränssnittet definiera operatorerna när det gäller typparametrarna. Typer som implementerar gränssnittet kan implicit implementera gränssnittsmetoderna.

Se även