Einschränkungen für Typparameter (C#-Programmierhandbuch)

Einschränkungen informieren den Compiler über die Funktionen, über die ein Typargument verfügen muss. Ohne Einschränkungen könnte das Typargument jedes beliebige Argument sein. Der Compiler kann nur die System.Object-Elemente annehmen. Dies ist die übergeordnete Basisklasse für jeden beliebigen .NET-Typ. Weitere Informationen finden Sie unter Weshalb Einschränkungen?. Wenn Clientcode einen Typ verwendet, der einen Constraint nicht erfüllt, gibt der Compiler einen Fehler aus. Constraints werden mit dem kontextuellen Schlüsselwort where angegeben. In der folgenden Tabelle werden die verschiedenen Einschränkungstypen aufgelistet:

Constraint Beschreibung
where T : struct Das Typargument muss ein Non-Nullable-Werttyp sein, der record struct-Typen enthält. Weitere Informationen zu Nullable-Werttypen finden Sie unter Nullable-Werttypen. Da alle Werttypen einen parameterlosen Konstruktor aufweisen, auf den entweder deklariert oder implizit zugegriffen werden kann, impliziert die struct-Einschränkung die new()-Einschränkung und kann nicht mit der new()-Einschränkung kombiniert werden. Ferner kann der struct-Constraint nicht mit dem unmanaged-Constraint kombiniert werden.
where T : class Das Typargument muss ein Verweistyp sein. Diese Einschränkung gilt auch für jede Klasse, Schnittstelle, jeden Delegaten oder Arraytyp. In einem Nullable-Kontext muss T ein Non-Nullable-Verweistyp sein.
where T : class? Das Typargument muss ein Nullable- oder ein Non-Nullable-Verweistyp sein. Diese Einschränkung gilt auch für jede Klasse, Schnittstelle, jeden Delegaten oder Arraytyp, einschließlich Datensätze.
where T : notnull Das Typargument muss ein Nicht-Nullable-Typ sein. Das Argument kann ein Non-Nullable-Verweistyp oder ein Non-Nullable-Werttyp sein.
where T : unmanaged Das Typargument muss ein nicht verwalteter Non-Nullable-Typ sein. Die unmanaged-Einschränkung impliziert die struct-Einschränkung und kann weder mit der struct- noch mit der new()-Einschränkung kombiniert werden.
where T : new() Das Typargument muss einen öffentlichen, parameterlosen Konstruktor aufweisen. Beim gemeinsamen Verwenden anderen Constraints muss der new()-Constraint zuletzt angegeben werden. Die new()-Einschränkung kann nicht mit der struct- oder unmanaged-Einschränkung kombiniert werden.
where T :<Basisklassenname> Das Typargument muss die angegebene Basisklasse sein oder von dieser abgeleitet werden. In einem Nullable-Kontext muss T ein Non-Nullable-Verweistyp sein, der von der angegebenen Basisklasse abgeleitet ist.
where T :<Basisklassenname>? Das Typargument muss die angegebene Basisklasse sein oder von dieser abgeleitet werden. In einem Nullable-Kontext kann T entweder ein Nullable- oder ein Non-Nullable-Typ sein, der von der angegebenen Basisklasse abgeleitet wird.
where T :<Schnittstellenname> Das Typargument muss die angegebene Schnittstelle sein oder diese implementieren. Es können mehrere Schnittstelleneinschränkungen angegeben werden. Die einschränkende Schnittstelle kann auch generisch sein. In einem Nullable-Kontext muss T ein Non-Nullable-Typ sein, der die angegebenen Schnittstelle implementiert.
where T :<Schnittstellenname>? Das Typargument muss die angegebene Schnittstelle sein oder diese implementieren. Es können mehrere Schnittstelleneinschränkungen angegeben werden. Die einschränkende Schnittstelle kann auch generisch sein. In einem Nullable-Kontext kann T ein Nullable-Verweistyp, ein Non-Nullable-Verweistyp oder ein Werttyp sein. T kann kein Nullable-Werttyp sein.
where T : U Das Typargument, das für T angegeben wird, muss das für U angegebene Argument sein oder von diesem abgeleitet werden. Wenn in einem Nullable-Kontext U ein Non-Nullable-Verweistyp ist, muss T ein Non-Nullable-Verweistyp sein. Wenn U ein Nullable-Verweistyp ist, kann T entweder ein Nullable- oder ein Non-Nullable-Typ sein.
where T : default Diese Einschränkung beseitigt die Mehrdeutigkeit, wenn Sie einen uneingeschränkten Typparameter angeben müssen, wenn Sie eine Methode überschreiben oder eine explizite Schnittstellenimplementierungen bereitstellen. Die default Einschränkung impliziert die Basismethode ohne die class oder struct -Einschränkung. Weitere Informationen finden Sie unter defaultHinweis zum Featurevorschlag.

Einige Einschränkungen schließen sich gegenseitig aus, und einige Einschränkungen müssen sich in einer bestimmten Reihenfolge befinden:

  • Sie können höchstens eine der Einschränkung struct, class, class?, notnull und unmanaged anwenden. Wenn Sie eine dieser Einschränkungen angeben, muss sie die erste Einschränkung sein, die für diesen Typparameter angegeben wird.
  • Die Einschränkung der Basisklasse (where T : Base oder where T : Base?) kann nicht mit einer der Einschränkungen struct, class, class?, notnull oder unmanaged kombiniert werden.
  • Sie können höchstens eine Basisklasseneinschränkung in beiden Formen anwenden. Wenn Sie den Nullable-Basistyp unterstützen möchten, verwenden Sie Base?.
  • Sie können nicht sowohl die Non-Nullable- als auch die Nullable-Form einer Schnittstelle als Einschränkung benennen.
  • Der new()-Constraint kann nicht mit dem struct- oder unmanaged-Constraint kombiniert werden. Wenn Sie die new()-Einschränkung angeben, muss sie die letzte Einschränkung für diesen Typparameter sein.
  • Die default-Einschränkung kann für eine Außerkraftsetzung oder eine explizite Schnittstellenimplementierung angewendet werden. Sie kann weder mit der struct- noch mit der class-Einschränkung kombiniert werden.

Weshalb Einschränkungen?

Constraints geben die Funktionen und Erwartungen eines Typparameters an. Das Deklarieren von Constraints bedeutet, dass Sie die Vorgänge und Methodenaufrufe des einschränkenden Typs verwenden können. Sie wenden Einschränkungen auf den Typparameter an, wenn Ihre generische Klasse oder Methode Vorgänge für generische Member verwendet, die über eine einfache Zuweisung hinausgehen, was das Aufrufen von Methoden beinhaltet, die nicht von System.Object unterstützt werden. Die Basisklasseneinschränkung sagt dem Compiler, dass nur Objekte dieses Typs oder Objekte, die von diesem Typ abgeleitet werden, dieses Typargument ersetzen können. Sobald der Compiler diese Garantie hat, kann er erlauben, dass Methoden dieses Typs in der generischen Klasse aufgerufen werden können. Im folgenden Codebeispiel wird die Funktionalität veranschaulicht, die der GenericList<T>-Klasse durch das Anwenden einer Basisklasseneinschränkung hinzugefügt werden kann (in Einführung in Generics).

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

Die Einschränkung ermöglicht der generischen Klasse, die Employee.Name-Eigenschaft zu verwenden. Die Einschränkung gibt an, dass alle Elemente des Typs T entweder ein Employee-Objekt oder ein Objekt sind, das von Employee erbt.

Mehrere Constraints können wie folgt auf den gleichen Typenparameter angewendet werden, und die Contraints können selbst generische Typen sein:

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

Wenn Sie die where T : class-Einschränkung anwenden, vermeiden Sie das Verwenden der Operatoren == und != mit dem Typparameter, da diese nur auf Verweisidentität und nicht auf Wertgleichheit prüfen. Dieses Verhalten tritt auch auf, wenn diese Operatoren in einem Typ überladen werden, der als Argument verwendet wird. Der folgende Code veranschaulicht diesen Aspekt. Die Ausgabe ist FALSE, obwohl die String-Klasse den ==-Operator überlädt.

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

Der Compiler weiß erst zur Kompilierzeit, dass T ein Verweistyp ist und die für alle Verweistypen zulässigen Standardoperatoren verwendet werden müssen. Wenn Sie auf Wertgleichheit prüfen müssen, wenden Sie die where T : IEquatable<T>- oder where T : IComparable<T>-Einschränkung an und implementieren Sie die Schnittstelle in jeder Klasse, die verwendet wird, um die generische Klasse zu erstellen.

Einschränken mehrerer Parameter

Sie können wie im folgenden Beispiel gezeigt Constraints auf mehrere Parameter und mehrere Constraints auf einen einzelnen Parameter anwenden:

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

Ungebundene Typparameter

Typparameter, auf die keine Constraints angewendet wurden, wie z.B. T in der öffentlichen Klasse SampleClass<T>{}, werden als ungebundene Typparameter bezeichnet. Für ungebundene Typparameter gelten die folgenden Regeln:

  • Die Operatoren != und == können nicht verwendet werden, weil es keine Garantie dafür gibt, dass das jeweilige Typargument diese auch unterstützt.
  • Sie können in und aus System.Object oder implizit in einen Schnittstellentyp konvertiert werden.
  • Sie können sie mit NULL vergleichen. Wenn ein ungebundener Parameter mit null verglichen wird, gibt der Vergleich immer FALSE zurück, wenn das Typargument ein Werttyp ist.

Typparameter als Einschränkungen

Es ist nützlich, einen Typparameter wie in folgendem Beispiel gezeigt als Constraint zu verwenden, wenn eine Memberfunktion mit ihren eigenen Typparametern diesen Parameter auf den Typparameter des enthaltenden Typs einschränken muss:

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

Im vorherigen Beispiel ist T ein Typconstraint im Kontext der Add-Methode und ein ungebundener Typparameter im Kontext der List-Klasse.

Typparameter können auch in generischen Klassendefinitionen als Constraints verwendet werden. Der Typparameter in spitzen Klammern muss zusammen mit allen anderen Typparametern deklariert werden:

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

Das Verwenden von Typparametern als Einschränkungen für generische Klassen ist nur bis zu einem gewissen Punkt nützlich, da der Compiler keine Informationen über den Typparameter annehmen kann, nur dass er von System.Object abgeleitet ist. Sie sollten Typparameter als Constraints dann verwenden, wenn Sie eine Vererbungsbeziehung zwischen zwei Typparametern erzwingen möchten.

notnull-Einschränkung

Sie können die notnull-Einschränkung verwenden, um anzugeben, dass das Typargument ein Non-Nullable-Werttyp oder Non-Nullable-Verweistyp sein muss. Im Gegensatz zu den meisten anderen Einschränkungen generiert der Compiler eine Warnung statt eines Fehlers, wenn ein Typargument die notnull-Einschränkung verletzt.

Die notnull-Einschränkung wirkt sich nur aus, wenn sie in einem Nullable-Kontext verwendet wird. Wenn Sie die notnull-Einschränkung in einem Nullable-Kontext hinzufügen, generiert der Compiler keine Warnungen oder Fehler für Verstöße gegen die Einschränkung.

class-Einschränkung

Die class-Einschränkung in einem Nullable-Kontext gibt an, dass das Typargument ein Non-Nullable-Verweistyp sein muss. In einem Nullable-Kontext generiert der Compiler eine Warnung, wenn ein Typargument ein Nullable-Verweistyp ist.

default-Einschränkung

Das Hinzufügen von Nullable-Verweistypen erschwert die Verwendung von T? in einem generischen Typ oder einer generischen Methode. T? kann entweder mit der struct- oder class- Einschränkung verwendet werden, eine von beiden muss aber vorhanden sein. Wenn die class Einschränkung verwendet wurde, T? verweist auf den Nullable-Verweistyp für T. T? kann verwendet werden, wenn keine Einschränkung angewendet wird. In diesem Fall wird T? für Werttypen und Verweistypen als T? interpretiert. Wenn jedoch T eine Instanz von Nullable<T> oder T? ist, ist sie mit T identisch. Anders ausgedrückt: Es wird nicht zu T??.

Da T? jetzt ohne dieclass- oder struct-Einschränkung verwendet werden kann, können Mehrdeutigkeiten in Überschreibungen oder expliziten Schnittstellenimplementierungen auftreten. In beiden Fällen enthält die Außerkraftsetzung nicht die Einschränkungen, sondern erhält sie von der Basisklasse. Wenn die Basisklasse weder die class- noch die struct-Einschränkung anwendet, müssen abgeleitete Klassen auf irgendeine Weise angeben, dass eine Außerkraftsetzung für die Basismethode ohne Einschränkung gilt. Die abgeleitete Methode wendet die default-Einschränkung an. Die default-Einschränkung verdeutlicht, dass weder dieclass weder noch die struct -Einschränkung.

Nicht verwaltete Einschränkungen

Sie können die unmanaged-Einschränkung nutzen, um anzugeben, dass der Typparameter ein nicht verwalteter Non-Nullable-Typ sein muss. Die unmanaged-Einschränkung ermöglicht Ihnen das Schreiben von wiederverwendbarer Routinen zum Arbeiten mit Typen, die als Speicherblöcke bearbeitet werden können, wie im folgenden Beispiel gezeigt:

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

Die vorherige Methode muss in einen unsafe-Kontext kompiliert werden, da sie den sizeof-Operator für einen nicht bekannten Typ verwendet, der ein integrierter Typ ist. Ohne die unmanaged-Einschränkung ist der sizeof-Operator nicht verfügbar.

Die unmanaged-Einschränkung impliziert die struct-Einschränkung und kann nicht mit ihr kombiniert werden. Da die struct-Einschränkung die new()-Einschränkung impliziert, kann die unmanaged-Einschränkung ebenso wenig mit der new()-Einschränkung kombiniert werden.

Delegieren von Einschränkungen

Sie können System.Delegate oder System.MulticastDelegate als Basisklasseneinschränkung verwenden. Die CLR lässt diese Einschränkung immer zu, aber die C#-Sprache lässt sie nicht zu. Die System.Delegate-Einschränkung ermöglicht es Ihnen, Code zu schreiben, der mit Delegaten in einer typsicheren Weise funktioniert. Der folgende Code definiert eine Erweiterungsmethode, die zwei Delegaten kombiniert, sofern diese vom gleichen Typ sind:

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

Sie können die oben dargestellte Methode verwenden, um Delegaten vom selben Typ zu kombinieren:

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

Wenn Sie den Kommentar der letzten Zeile entfernen, findet die Kompilation nicht statt. Sowohl first als auch test sind Delegattypen, aber sie sind unterschiedlich.

Enumerationseinschränkungen

Sie können auch den System.Enum-Typ als Basisklasseneinschränkung angeben. Die CLR lässt diese Einschränkung immer zu, aber die C#-Sprache lässt sie nicht zu. Generics, die System.Enum verwenden, bieten typsichere Programmierung zum Zwischenspeichern von Ergebnissen aus der Verwendung der statischen Methoden in System.Enum. Im folgenden Beispiel werden alle gültigen Werte für einen Enumerationstyp gefunden, und dann ein Wörterbuch erstellt, das diese Werte ihrer Zeichenfolgendarstellung zuordnet.

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 und Enum.GetName nutzen Reflektion, die Auswirkungen auf die Leistung hat. Sie können EnumNamedValues aufrufen, um eine Sammlung zu erstellen, die zwischengespeichert und wiederverwendet wird, anstatt die Aufrufe zu wiederholen, die eine Reflektion erfordern.

Sie könnten dies wie im folgenden Beispiel gezeigt verwenden, um eine Enumeration und ein Wörterbuch der Werte und Namen zu erstellen:

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

Typargumente implementieren deklarierte Schnittstelle

In einigen Szenarien ist es erforderlich, dass ein für einen Typparameter angegebenes Argument die betreffende Schnittstelle implementiert. Beispiel:

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

Dieses Muster ermöglicht es dem C#-Compiler, den enthaltenden Typ für die überladenen Operatoren oder eine beliebige static virtual- oder static abstract-Methode zu bestimmen. Es stellt die Syntax bereit, sodass die Additions- und Subtraktionsoperatoren für einen enthaltenden Typ definiert werden können. Ohne diese Einschränkung müssten die Parameter und Argumente als Schnittstelle und nicht als Typparameter deklariert werden:

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

Die obige Syntax erfordert, dass Implementierer eine explizite Schnittstellenimplementierung für diese Methoden verwenden. Durch Angabe der zusätzlichen Einschränkung kann die Schnittstelle die Operatoren in Bezug auf die Typparameter definieren. Typen, die die Schnittstelle implementieren, können die Schnittstellenmethoden implizit implementieren.

Weitere Informationen