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 U elementu . 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ń . |
where T : allows ref struct |
To ograniczenie anty-ograniczenie deklaruje, że argument T typu może być typem ref struct . Typ ogólny lub metoda musi przestrzegać reguł bezpieczeństwa ref dla dowolnego wystąpienia, T ponieważ może to być ref struct . |
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
struct
ograniczeń , ,class
class?
,notnull
iunmanaged
. Jeśli podasz dowolne z tych ograniczeń, musi to być pierwsze ograniczenie określone dla tego typu parametru. - Ograniczenie klasy bazowej (
where T : Base
lubwhere T : Base?
) nie może być łączone z żadnymi ograniczeniamistruct
, ,class
class?
,notnull
lubunmanaged
. - 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 ograniczeniemstruct
lubunmanaged
. Jeśli określisznew()
ograniczenie, musi to być ostatnie ograniczenie dla tego parametru typu. Ograniczenia, jeśli ma to zastosowanie, mogą być zgodne z ograniczeniemnew()
.- Ograniczenie
default
można stosować tylko w przypadku implementacji zastępowania lub jawnego interfejsu. Nie można połączyć jej zstruct
ograniczeniami lubclass
. - Nie
allows ref struct
można połączyć ograniczeń antywłaściwych z ograniczeniemclass
lubclass?
. - Ograniczenie
allows ref struct
anty-ograniczenie musi być zgodne ze wszystkimi ograniczeniami dla tego parametru typu.
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 Employee
klasy .
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 : notnull, Employee, IComparable<T>, new()
{
// ...
public void AddDefault()
{
T t = new T();
// ...
}
}
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
null
parametrem , 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.Object
klasy . 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, ani class
struct
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;
Możesz użyć powyższej metody, aby 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 on 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>
{
static abstract T operator +(T left, T right);
static abstract 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>
{
static abstract IAdditionSubtraction<T> operator +(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
static abstract 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.
Zezwala na strukturę ref
Ograniczenie allows ref struct
anty-ograniczenie deklaruje, że odpowiedni argument typu może być typem ref struct
. Wystąpienia tego parametru typu muszą przestrzegać następujących reguł:
- Nie można go boksować.
- Uczestniczy w zasadach bezpieczeństwa ref.
- Nie można używać wystąpień, w których
ref struct
typ nie jest dozwolony, na przykładstatic
pola. - Wystąpienia można oznaczyć modyfikatorem
scoped
.
Klauzula allows ref struct
nie jest dziedziczona. W poniższym kodzie:
class SomeClass<T, S>
where T : allows ref struct
where S : T
{
// etc
}
Argument argumentu nie S
może być argumentem ref struct
, ponieważ S
nie ma klauzuli allows ref struct
.
Nie można użyć parametru allows ref struct
typu, który zawiera klauzulę jako argument typu, chyba że odpowiedni parametr typu ma również klauzulę allows ref struct
. Ta reguła jest pokazana w poniższym przykładzie:
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
}
W poprzednim przykładzie pokazano, że argument typu, który może być typem ref struct
, nie może zostać zastąpiony parametrem typu, który nie może być typem ref struct
.