Ograniczenia dotyczące parametrów typu (Przewodnik programowania w języku C#)

Ograniczenia informują kompilator o możliwościach, które musi zawierać argument typu. Bez żadnych ograniczeń argument typu może być dowolnym typem. Kompilator może przyjąć tylko elementy członkowskie System.Objectklasy , która jest ostateczną klasą bazową dla dowolnego typu platformy .NET. Aby uzyskać więcej informacji, zobacz Dlaczego warto używać ograniczeń. Jeśli kod klienta używa typu, który nie spełnia ograniczeń, kompilator zgłasza błąd. Ograniczenia są określane przy użyciu kontekstowego słowa kluczowego where . W poniższej tabeli wymieniono różne typy ograniczeń:

Ograniczenie opis
where T : struct Argument typu musi być typem wartości bez wartości null, który zawiera record struct typy. Aby uzyskać informacje o typach wartości dopuszczanych do wartości null, zobacz Typy wartości dopuszczanych do wartości null. Ponieważ wszystkie typy wartości mają dostępny konstruktor bez parametrów, zadeklarowany lub niejawny, struct ograniczenie oznacza new() ograniczenie i nie można go połączyć z ograniczeniem new() . Nie można połączyć struct ograniczenia z ograniczeniem unmanaged .
where T : class Argument typu musi być typem odwołania. To ograniczenie dotyczy również dowolnej klasy, interfejsu, delegata lub typu tablicy. W kontekście T dopuszczalnym wartości null musi być typem odwołania bez wartości null.
where T : class? Argument typu musi być typem odwołania, dopuszczanym do wartości null lub bez wartości null. To ograniczenie dotyczy również dowolnej klasy, interfejsu, delegata lub typu tablicy, w tym rekordów.
where T : notnull Argument typu musi być typem niepustym. Argument może być typem odwołania nienależące do wartości null lub typem wartości innej niż null.
where T : unmanaged Argument typu musi być typem niezarządzanym bez wartości null. Ograniczenie unmanaged oznacza struct ograniczenie i nie można go połączyć z struct ograniczeniami lub new() .
where T : new() Argument typu musi mieć publiczny konstruktor bez parametrów. W przypadku użycia razem z innymi ograniczeniami new() ograniczenie musi być określone jako ostatnie. Ograniczenie new() nie może być łączone z struct ograniczeniami i unmanaged .
where T :<nazwa klasy bazowej> Argument typu musi być lub pochodzić z określonej klasy bazowej. W kontekście T dopuszczalnym wartości null musi być niepustym typem odwołania pochodzącym z określonej klasy bazowej.
where T :<nazwa> klasy bazowej? Argument typu musi być lub pochodzić z określonej klasy bazowej. W kontekście T dopuszczania wartości null może być typem dopuszczalnym do wartości null lub innym niż null pochodzącym z określonej klasy bazowej.
where T :<nazwa interfejsu> Argument typu musi być lub zaimplementować określony interfejs. Można określić wiele ograniczeń interfejsu. Interfejs ograniczający może być również ogólny. W kontekście dopuszczania wartości null musi być typem niepustym T , który implementuje określony interfejs.
where T :<nazwa> interfejsu? Argument typu musi być lub zaimplementować określony interfejs. Można określić wiele ograniczeń interfejsu. Interfejs ograniczający może być również ogólny. W kontekście T dopuszczalnym wartości null może być typem odwołania dopuszczanym do wartości null, typem referencyjnym innym niż null lub typem wartości. T nie może być typem wartości dopuszczanej do wartości null.
where T : U Argument typu podany dla T elementu musi być lub pochodzić z argumentu podanego dla Uelementu . W kontekście dopuszczalnym wartości null, jeśli U jest typem odwołania nienależące do wartości null, musi być typem odwołania bez T wartości null. Jeśli U jest typem odwołania dopuszczanym do wartości null, T może mieć wartość null lub wartość inną niż null.
where T : default To ograniczenie rozwiązuje niejednoznaczność, gdy trzeba określić nieskonspirowany parametr typu podczas zastępowania metody lub zapewnienia jawnej implementacji interfejsu. Ograniczenie default oznacza metodę podstawową bez class ograniczenia lub struct . Aby uzyskać więcej informacji, zobacz propozycję specyfikacji default ograniczeń .

Niektóre ograniczenia wzajemnie się wykluczają, a niektóre ograniczenia muszą być w określonej kolejności:

  • Można zastosować co najwyżej jeden z structograniczeń , , classclass?, notnulli unmanaged . Jeśli podasz dowolne z tych ograniczeń, musi to być pierwsze ograniczenie określone dla tego typu parametru.
  • Ograniczenie klasy bazowej (where T : Baselub ) nie może być łączone z żadnymi ograniczeniami struct, , class, class?notnull, lub unmanagedwhere T : Base?.
  • W obu formach można zastosować co najwyżej jedno ograniczenie klasy bazowej. Jeśli chcesz obsługiwać typ podstawowy dopuszczany do wartości null, użyj polecenia Base?.
  • Nie można nazwać zarówno niepustej, jak i dopuszczanej do wartości null postaci interfejsu jako ograniczenia.
  • new() Ograniczenie nie może być łączone z ograniczeniem struct lub unmanaged . Jeśli określisz new() ograniczenie, musi to być ostatnie ograniczenie dla tego parametru typu.
  • Ograniczenie default można stosować tylko w przypadku implementacji zastępowania lub jawnego interfejsu. Nie można połączyć jej z struct ograniczeniami lub class .

Dlaczego warto używać ograniczeń

Ograniczenia określają możliwości i oczekiwania parametru typu. Deklarowanie tych ograniczeń oznacza, że można użyć operacji i wywołań metod typu ograniczenia. Ograniczenia są stosowane do parametru typu, gdy klasa ogólna lub metoda używa dowolnej operacji na składowych ogólnych poza prostym przypisaniem, co obejmuje wywoływanie żadnych metod, które nie są obsługiwane przez System.Objectprogram . Na przykład ograniczenie klasy bazowej informuje kompilator, że tylko obiekty tego typu lub pochodzące z tego typu mogą zastąpić ten argument typu. Gdy kompilator ma tę gwarancję, może zezwolić na wywoływanie metod tego typu w klasie ogólnej. W poniższym przykładzie kodu pokazano funkcjonalność, którą można dodać do GenericList<T> klasy (w temacie Introduction to Generics), stosując ograniczenie klasy bazowej.

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

Ograniczenie umożliwia klasie ogólnej używanie Employee.Name właściwości . Ograniczenie określa, że wszystkie elementy typu T mają gwarancję, Employee że obiekt lub obiekt dziedziczy z Employeeklasy .

Do tego samego parametru typu można zastosować wiele ograniczeń, a same ograniczenia mogą być typami ogólnymi w następujący sposób:

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

Podczas stosowania where T : class ograniczenia należy unikać == operatorów i != dla parametru typu, ponieważ te operatory testować tylko tożsamość referencyjną, a nie dla równości wartości. To zachowanie występuje nawet wtedy, gdy te operatory są przeciążone w typie, który jest używany jako argument. Poniższy kod ilustruje ten punkt; dane wyjściowe są fałszywe, mimo że String klasa przeciąża == operatora.

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

Kompilator wie tylko, że T jest to typ odwołania w czasie kompilacji i musi używać operatorów domyślnych, które są prawidłowe dla wszystkich typów odwołań. Jeśli musisz przetestować równość wartości, zastosuj where T : IEquatable<T> ograniczenie lub where T : IComparable<T> i zaimplementuj interfejs w dowolnej klasie używanej do konstruowania klasy ogólnej.

Ograniczanie wielu parametrów

Ograniczenia można zastosować do wielu parametrów i wiele ograniczeń do jednego parametru, jak pokazano w poniższym przykładzie:

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

Parametry typu niezwiązanego

Parametry typu, które nie mają ograniczeń, takich jak T w klasie SampleClass<T>{}publicznej , są nazywane parametrami typu bez ruchu przychodzącego. Parametry typu bez ruchu mają następujące reguły:

  • != Operatory i == nie mogą być używane, ponieważ nie ma gwarancji, że argument typu konkretnego obsługuje te operatory.
  • Można je przekonwertować na i i z System.Object lub jawnie przekonwertować na dowolny typ interfejsu.
  • Można je porównać z wartością null. Jeśli niezwiązany parametr jest porównywany z nullparametrem , porównanie zawsze zwraca wartość false, jeśli argument typu jest typem wartości.

Parametry typu jako ograniczenia

Użycie parametru typu ogólnego jako ograniczenia jest przydatne, gdy funkcja składowa z własnym parametrem typu musi ograniczyć ten parametr do parametru typu zawierającego typ, jak pokazano w poniższym przykładzie:

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

W poprzednim przykładzie T jest ograniczeniem typu w kontekście Add metody i niezwiązanym parametrem typu w kontekście List klasy.

Parametry typu mogą być również używane jako ograniczenia w definicjach klas ogólnych. Parametr typu musi być zadeklarowany w nawiasach kątowych wraz z innymi parametrami typu:

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

Użyteczność parametrów typu jako ograniczeń z klasami ogólnymi jest ograniczona, ponieważ kompilator nie może założyć nic o parametrze typu, z wyjątkiem tego, że pochodzi z System.Objectklasy . Użyj parametrów typu jako ograniczeń dla klas ogólnych w scenariuszach, w których chcesz wymusić relację dziedziczenia między dwoma parametrami typu.

notnull Ograniczenie

Możesz użyć notnull ograniczenia, aby określić, że argument typu musi być typem wartości innej niż null lub typem odwołania bez wartości null. W przeciwieństwie do większości innych ograniczeń, jeśli argument typu narusza notnull ograniczenie, kompilator generuje ostrzeżenie zamiast błędu.

Ograniczenie notnull ma wpływ tylko wtedy, gdy jest używane w kontekście dopuszczalnym wartości null. Jeśli dodasz notnull ograniczenie w kontekście niezwiązanym z wartością null, kompilator nie generuje żadnych ostrzeżeń ani błędów w przypadku naruszeń ograniczenia.

class Ograniczenie

Ograniczenie class w kontekście dopuszczający wartość null określa, że argument typu musi być typem odwołania nienależącym do wartości null. W kontekście dopuszczania wartości null, gdy argument typu jest typem odwołania dopuszczanym do wartości null, kompilator generuje ostrzeżenie.

default Ograniczenie

Dodanie typów odwołań dopuszczanych do wartości null komplikuje użycie elementu T? w typie ogólnym lub metodzie. T? może być używany z ograniczeniem struct lub class , ale jeden z nich musi być obecny. class Gdy ograniczenie zostało użyte, T? odwołuje się do typu odwołania dopuszczanego do wartości null dla elementu T. T? można użyć, gdy żadne ograniczenie nie jest stosowane. W takim przypadku T? jest interpretowany jako T? typy wartości i typy referencyjne. Jeśli T jednak jest wystąpieniem Nullable<T>klasy , T? jest takie samo jak T. Innymi słowy, nie staje się .T??

Ponieważ T? można teraz używać bez class ograniczeń lub struct , niejednoznaczności mogą wystąpić w przesłonięciach lub jawnych implementacjach interfejsu. W obu tych przypadkach przesłonięcia nie obejmują ograniczeń, ale dziedziczą je z klasy bazowej. Gdy klasa bazowa nie stosuje class ani ograniczenia, struct klasy pochodne muszą w jakiś sposób określić przesłonięcia stosowane do metody podstawowej bez ograniczeń. Metoda pochodna stosuje default ograniczenie. Ograniczenie default nie wyjaśnia ani ograniczenia, aniclassstruct ograniczenia.

Ograniczenie niezarządzane

Za pomocą unmanaged ograniczenia można określić, że parametr typu musi być typem niezarządzanym bez wartości null. Ograniczenie unmanaged umożliwia pisanie procedur wielokrotnego użytku do pracy z typami, które mogą być manipulowane jako bloki pamięci, jak pokazano w poniższym przykładzie:

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

Poprzednia metoda musi być skompilowana w unsafe kontekście, ponieważ używa sizeof operatora w typie, który nie jest znany jako typ wbudowany. unmanaged Bez ograniczenia sizeof operator jest niedostępny.

Ograniczenie unmanaged oznacza struct ograniczenie i nie można go połączyć. struct Ponieważ ograniczenie oznacza new() ograniczenie, unmanaged ograniczenie nie może być również połączone z ograniczeniemnew().

Delegowanie ograniczeń

Możesz użyć System.Delegate ograniczenia klasy bazowej lub System.MulticastDelegate jako ograniczenia klasy bazowej. ClR zawsze zezwalał na to ograniczenie, ale język C# go nie zezwalał. Ograniczenie System.Delegate umożliwia pisanie kodu, który współpracuje z delegatami w bezpieczny sposób. Poniższy kod definiuje metodę rozszerzenia, która łączy dwa delegaty pod warunkiem, że są tego samego typu:

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

Za pomocą powyższej metody można połączyć delegaty, które są tego samego typu:

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

Jeśli anulujesz komentarz z ostatniego wiersza, nie zostanie skompilowany. Oba first typy test i są typami delegatów, ale są różne typy delegatów.

Ograniczenia wyliczenia

Można również określić System.Enum typ jako ograniczenie klasy bazowej. ClR zawsze zezwalał na to ograniczenie, ale język C# go nie zezwalał. Typy ogólne korzystające z System.Enum funkcji zapewniają bezpieczne programowanie typu w celu buforowania wyników z używania metod statycznych w programie System.Enum. Poniższy przykład znajduje wszystkie prawidłowe wartości dla typu wyliczenia, a następnie tworzy słownik, który mapuje te wartości na jego reprezentację ciągu.

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 i Enum.GetName stosować odbicie, które ma wpływ na wydajność. Możesz wywołać EnumNamedValues metodę w celu utworzenia kolekcji, która jest buforowana i ponownie użyta, zamiast powtarzać wywołania wymagające odbicia.

Można go użyć, jak pokazano w poniższym przykładzie, aby utworzyć wyliczenie i utworzyć słownik jego wartości i nazwy:

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

Argumenty typu implementują zadeklarowany interfejs

Niektóre scenariusze wymagają, aby argument podany dla parametru typu implementować ten interfejs. Na przykład:

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

Ten wzorzec umożliwia kompilatorowi języka C# określenie typu zawierającego dla przeciążonych operatorów lub dowolnej static virtual metody lub static abstract . Udostępnia składnię, dzięki czemu operatory dodawania i odejmowania można zdefiniować w typie zawierającym. Bez tego ograniczenia parametry i argumenty muszą być zadeklarowane jako interfejs, a nie parametr typu:

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

Poprzednia składnia wymagałaby od implementatorów używania jawnej implementacji interfejsu dla tych metod. Zapewnienie dodatkowego ograniczenia umożliwia interfejsowi definiowanie operatorów pod względem parametrów typu. Typy implementujące interfejs mogą niejawnie implementować metody interfejsu.

Zobacz też