Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Ограничения сообщают компилятору о характеристиках, которые должен иметь аргумент типа. Без ограничений аргумент типа может быть любым типом. Компилятор может только предполагать членов System.Object, который является главным базовым классом для всех типов .NET. Дополнительные сведения см. в статье Зачем использовать ограничения. Если клиентский код использует тип, который не удовлетворяет ограничению, компилятор выдает ошибку. Ограничения задаются с помощью контекстного ключевого слова where. В следующей таблице описываются различные типы ограничений:
| Ограничение | Описание |
|---|---|
where T : struct |
Аргумент типа должен быть типом значения, который не может принимать значение null и включает record struct типы. См. дополнительные сведения о типах значений, допускающих значение NULL. Так как все типы значений имеют конструктор без параметров, объявленный или неявный, struct ограничение подразумевает new() ограничение и не может сочетаться с ограничением new() . Ограничение struct нельзя использовать вместе с ограничением unmanaged. |
where T : class |
Аргумент типа должен быть ссылочным типом. Это ограничение также применяется к любому типу класса, интерфейса, делегата или массива. В контексте, допускающем значение NULL, T должен быть ссылочным типом, не допускающим значения NULL. |
where T : class? |
Тип аргумента должен быть ссылочным и может либо поддерживать значения NULL, либо не поддерживать их. Это ограничение применяется также к любому классу, интерфейсу, делегату или типу массива, включая записи. |
where T : notnull |
Аргумент типа должен быть типом, не допускающим значения NULL. Аргумент может быть ненулевым ссылочным типом или ненулевым типом значения. |
where T : unmanaged |
Аргумент типа должен быть неуправляемым типом, не допускающим значения NULL. Ограничение unmanaged подразумевает ограничение struct и не может использоваться совместно с ограничением struct или new(). |
where T : new() |
Аргумент типа должен иметь общий конструктор без параметров. При использовании совместно с другими ограничениями, ограничение new() должно указываться последним. Ограничение new() не может использоваться с ограничениями struct и unmanaged. |
where T :
<Имя базового класса> |
Аргумент типа должен быть указанным базовым классом или производным от него. В контексте, допускающем значение NULL, T должен быть ссылочным типом, который не допускает значение NULL, производным от указанного базового класса. |
where T :
<имя> базового класса? |
Аргумент типа должен быть указанным базовым классом или наследоваться от него. В контексте, допускающем значение NULL, T может быть либо типом, допускаемым null, либо не допускаемым значением NULL, производным от указанного базового класса. |
where T :
<имя интерфейса> |
Аргумент типа должен являться заданным интерфейсом или реализовывать его. Можно указать несколько ограничений интерфейса. Ограничивающий интерфейс также может быть общим. В контексте, допускающем значение NULL, T должен быть ненулевым типом, реализующим указанный интерфейс. |
where T :
<имя> интерфейса? |
Аргумент типа должен являться заданным интерфейсом или реализовывать его. Можно указать несколько ограничений интерфейса. Ограничительный интерфейс также может быть обобщенным. В nullable-контексте T может быть nullable-ссылочным типом, non-nullable-ссылочным типом или типом значения.
T не может быть типом значений, допускающим значение NULL. |
where T : U |
Аргумент типа, указанный для T, должен быть аргументом, указанным для U, или производным от него. В контексте, допускающем значение NULL, если U это ненулевой ссылочный тип, T должен быть ненулевой ссылочный тип. Если U это ссылочный тип, допускающий значение NULL, T может иметь значение NULL или не допускающее значение NULL. |
where T : default |
Это ограничение устраняет неоднозначность, если необходимо указать неограниченный параметр типа, переопределяя метод или указывая явную реализацию интерфейса. Ограничение default подразумевает базовый метод без ограничения class или struct. Дополнительные сведения см. в предложении спецификации ограничения default. |
where T : allows ref struct |
Это анти-ограничение объявляет, что аргумент типа T может быть типом ref struct. Универсальный тип или метод должен соответствовать правилам безопасности ссылок для любого экземпляра T , так как это может быть ref struct. |
Некоторые ограничения являются взаимоисключающими, и некоторые ограничения должны быть в указанном порядке:
- Вы можете применить не более одного из ограничений
struct,class,class?,notnullилиunmanaged. Если вы предоставляете любой из этих ограничений, это должно быть первое ограничение, указанное для этого параметра типа. - Ограничение базового класса (
where T : Baseилиwhere T : Base?) не может сочетаться с какими-либо из ограниченийstruct,class,class?,notnullилиunmanaged. - Можно применить не более одного ограничения базового класса в любой форме. Если вы хотите поддерживать базовый тип, допускающий значение NULL, используйте
Base?. - Вы не можете использовать как ненулевую, так и nullable-форму интерфейса в качестве ограничения.
- Ограничение
new()нельзя использовать с ограничениемstructилиunmanaged. Если указатьnew()ограничение, это должно быть последнее ограничение для этого параметра типа. Анти-ограничения могут, если применимо, следовать ограничениюnew(). - Ограничение
defaultможет применяться только к переопределениям или явным реализациям интерфейса. Его нельзя объединить сstructограничениями илиclassограничениями. - Анти-ограничение
allows ref structне может быть объединено сclassилиclass?ограничением. - Анти-ограничение
allows ref structдолжно соответствовать всем ограничениям для этого параметра типа.
Зачем использовать ограничения
Ограничения определяют возможности и ожидания параметра типа. Объявление этих ограничений означает, что можно использовать операции и вызовы методов ограничивающего типа. Ограничения применяются к параметру типа, когда универсальный класс или метод использует любую операцию с универсальными элементами за пределами простого назначения, которая включает вызов любых методов, не поддерживаемых System.Object. Например, ограничение базового класса сообщает компилятору, что только объекты этого типа или производные от этого типа могут заменить этот аргумент типа. Имея такую гарантию, компилятор может вызывать методы указанного типа в универсальном классе. В следующем примере кода показаны функциональные возможности, которые можно добавить в класс GenericList<T> (см. раздел Введение в универсальные шаблоны), применив ограничение базового класса.
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(t) { Next = head };
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node? current = head;
while (current is not null)
{
yield return current.Data;
current = current.Next;
}
}
public T? FindFirstOccurrence(string s)
{
Node? current = head;
while (current is not null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
return current.Data;
}
else
{
current = current.Next;
}
}
return null;
}
}
Это ограничение позволяет универсальному классу использовать свойство Employee.Name. Ограничение указывает, что все элементы типа T гарантированно являются либо объектом Employee, либо объектом, который наследует от Employee.
К одному параметру типа можно применять несколько ограничений, которые сами по себе могут быть универсальными типами, как показано ниже:
class EmployeeList<T> where T : notnull, Employee, IComparable<T>, new()
{
public void AddDefault()
{
T t = new();
}
}
При применении where T : class ограничения избегайте == и != операторов для параметра типа, так как эти операторы проверяют только идентичность ссылок, а не равенство значений. Такое поведение будет наблюдаться даже в том случае, если эти операторы будут перегружены в типе, используемом в качестве аргумента. Эта особенность показана в следующем коде, который будет возвращать значение false даже в том случае, если класс String перегружает оператор ==.
public static void OpEqualsTest<T>(T s, T t) where T : class
{
Console.WriteLine(s == t);
}
private static void TestStringEquality()
{
string s1 = "target";
System.Text.StringBuilder sb = new("target");
string s2 = sb.ToString();
OpEqualsTest(s1, s2);
}
Компилятору известно только то, что T является ссылочным типом во время компиляции, и он должен использовать операторы по умолчанию, которые действительны для всех ссылочных типов. Если необходимо проверить равенство значений, примените where T : IEquatable<T> или where T : IComparable<T> ограничение и реализуйте интерфейс в любом классе, используемом для создания универсального класса.
Ограничение нескольких параметров
Ограничения можно применить к нескольким параметрам. Кроме того, к одному параметру можно применять несколько ограничений, как показано в следующем примере:
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
Несвязанные параметры типа
Не имеющие ограничений параметры типа (например, T в общем классе SampleClass<T>{}) называются несвязанными. В отношении несвязанных параметров типа применяются следующие правила:
- Операторы
!=и==нельзя использовать, так как нет гарантии, что аргумент конкретного типа поддерживает их. - Их можно преобразовывать из и в
System.Object, а также явно преобразовывать в любой тип интерфейса. - Можно сравнить их со значением null. Если несвязанный параметр сравнивается
null, сравнение всегда возвращает значение false, если аргумент типа является типом значения.
Параметры типа в качестве ограничений
Использование параметров универсального типа в качестве ограничений применимо, когда функция-член со своим параметром типа должна ограничивать этот параметр параметром содержащего типа, как показано в следующем примере:
public class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}
В предыдущем примере T является ограничением типа в контексте метода Add и несвязанным параметром типа в контексте класса List.
Параметры типа также можно использовать в определениях универсальных классов. Параметр типа необходимо объявлять в угловых скобках вместе с любыми другими параметрами типа:
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
Применение параметров типа в качестве ограничений для универсальных классов ограничено, поскольку в отношении таких параметров типа компилятор может предполагать только то, что они являются производными от System.Object. Параметры типа в качестве ограничений следует использовать в универсальных классах в тех случаях, когда необходимо обеспечить отношение наследования между двумя параметрами типа.
Ограничение notnull
Ограничение можно использовать notnull для указания того, что аргумент типа должен быть типом значения, не допускающим значение NULL, или ссылочным типом, не допускающим значение NULL. В отличие от большинства других ограничений, если аргумент типа нарушает ограничение notnull, компилятор генерирует предупреждение вместо ошибки.
Ограничение notnull действует только при использовании в контексте, допускающем значения NULL. При добавлении ограничения notnull в очевидный контекст, допускающий значения NULL, компилятор не создает никаких предупреждений или ошибок в случае нарушений ограничения.
Ограничение class
Ограничение class в контексте, допускающем значение NULL, указывает, что аргумент типа должен быть ссылочным типом, не допускающим значение NULL. Если в контексте, допускающем значения NULL, аргумент типа является ссылочным типом, допускающим значения NULL, компилятор выдаст предупреждение.
Ограничение default
Добавление ссылочных типов, допускающих значения NULL, усложняет использование T? для универсального типа или метода.
T? можно использовать либо с ограничением struct, либо с class, но одно из них должно присутствовать. Если используется ограничение class, T? ссылается на ссылочный тип, допускающий значения NULL, для T.
T? можно использовать, если ни в коем случае не применяется ограничение. В этом случае T? интерпретируется как T? для типов значений и ссылочных типов. Но если T — экземпляр Nullable<T>, T? соответствует T. Другими словами, оно не становится T??.
Так как T? теперь можно использовать без ограничения class или struct, в переопределениях или явных реализациях интерфейса могут возникать неоднозначности. В обоих этих случаях переопределение не включает ограничения, а унаследует их от базового класса. Если базовый класс не применяет ограничение class или struct, производные классы должны каким-либо образом указывать переопределение, применяемое к базовому методу без ограничения. Производный default метод применяет ограничение. Ограничение defaultне уточняет ни ограничение class, ни struct.
Неуправляемое ограничение
Ограничение можно использовать unmanaged для указания того, что параметр типа должен быть неуправляемым типом, не допускающим значение NULL. Ограничение unmanaged позволяет создавать многократно используемые подпрограммы для работы с типами, которые могут обрабатываться как блоки памяти, как показано в следующем примере:
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;
}
В примере выше метод необходимо компилировать в контексте unsafe, так как он использует оператор sizeof для типа, не известного как встроенный тип. Без ограничения unmanaged оператор sizeof недоступен.
Ограничение unmanaged подразумевает ограничение struct и не может использоваться совместно с ним. Поскольку ограничение struct подразумевает ограничение new(), ограничение unmanaged также не может использоваться с ограничением new().
Ограничения делегата
Можно использовать System.Delegate или System.MulticastDelegate в качестве ограничения базового класса. Среда CLR всегда разрешала это ограничение, но язык C# запрещал его. Ограничение System.Delegate позволяет написать код, который работает с делегатами типобезопасным образом. Следующий код определяет метод расширения, который объединяет два делегата при условии, что они одного типа:
extension<TDelegate>(TDelegate source) where TDelegate : System.Delegate
{
public TDelegate? TypeSafeCombine(TDelegate target)
=> Delegate.Combine(source, target) as TDelegate;
}
Предыдущий метод можно использовать для объединения делегатов, которые являются одинаковыми типами:
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);
Если вы раскомментируете последнюю строку, она не компилируется.
first и test являются типами делегатов, но это разные типы делегатов.
Ограничения перечисления
Можно также указать System.Enum тип в качестве ограничения базового класса. В среде CLR это ограничение всегда было разрешено, но в языке C# оно было запрещено. Универсальные шаблоны с System.Enum предоставляют типобезопасное программирование для кэширования результатов использования статических методов в System.Enum. В следующем примере выполняется поиск всех допустимых значений для типа перечисления и создается словарь, который сопоставляет эти значения с их строковым представлением.
extension<T>(T) where T : System.Enum
{
public static Dictionary<int, string> EnumNamedValues()
{
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 и Enum.GetName используют отражение, которое влияет на производительность. Вы можете не повторять вызовы, требующие отражения, а вызвать EnumNamedValues для создания коллекции, которая кэшируется и используется повторно.
Вы можете использовать его, как показано в следующем примере, для создания перечисления и построения словаря значений и имен этого перечисления.
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}");
Аргументы типа реализуют объявленный интерфейс
В некоторых сценариях требуется, чтобы аргумент, предоставленный для параметра типа, реализул этот интерфейс. Например:
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
static abstract T operator +(T left, T right);
static abstract T operator -(T left, T right);
}
Этот шаблон позволяет компилятору C# определить содержащий тип для перегруженных операторов или любого static virtual или static abstract метода. Он предоставляет синтаксис, чтобы операторы добавления и вычитания могли быть определены для содержащего типа. Без этого ограничения параметры и аргументы должны быть объявлены в качестве интерфейса, а не параметр типа:
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);
}
Предыдущий синтаксис требует, чтобы разработчики использовали явную реализацию интерфейса для этих методов. Предоставление дополнительного ограничения позволяет интерфейсу определять операторы с точки зрения параметров типа. Типы, реализующие интерфейс, могут неявно реализовать методы интерфейса.
Разрешает использование ref struct
Анти-ограничение allows ref struct объявляет, что соответствующий аргумент типа может быть типом ref struct . Экземпляры этого параметра типа должны соответствовать следующим правилам:
- Он не может быть упакован.
- Он участвует в правилах безопасности ссылок.
- Экземпляры нельзя использовать, если
ref structтип не разрешен, напримерstaticполя. - Экземпляры можно пометить модификатором
scoped.
Предложение allows ref struct не наследуется. В приведенном ниже коде выполняется следующее:
class SomeClass<T, S>
where T : allows ref struct
where S : T
{
// etc
}
Аргумент для S не может являться ref struct, так как S не имеет условия allows ref struct.
Параметр типа, имеющий allows ref struct предложение, нельзя использовать в качестве аргумента типа, если соответствующий параметр типа также не содержит allows ref struct предложение. Это правило демонстрируется в следующем примере:
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
}
В предыдущем примере показано, что аргумент типа, который может быть типом ref struct , не может быть заменен параметром типа, который не может быть типом ref struct .