Ограничения параметров типа (руководство по программированию на C#)
Ограничения сообщают компилятору о характеристиках, которые должен иметь аргумент типа. Без ограничений аргумент типа может быть любым типом. Компилятор может только предполагать членов System.Object, который является главным базовым классом для всех типов .NET. Дополнительные сведения см. в статье Зачем использовать ограничения. Если клиентский код использует тип, который не удовлетворяет ограничению, компилятор выдает ошибку. Ограничения задаются с помощью контекстного ключевого слова where
. В следующей таблице описываются различные типы ограничений:
Ограничение | Description |
---|---|
where T : struct |
Аргумент типа должен быть ненулевым типом значения, который включает record struct типы. См. дополнительные сведения о типах значений, допускающих значение NULL. Так как все типы значений имеют конструктор без параметров, объявленный или неявный, struct ограничение подразумевает new() ограничение и не может сочетаться с ограничением new() . Ограничение struct нельзя использовать вместе с ограничением unmanaged . |
where T : class |
Аргумент типа должен быть ссылочным типом. Это ограничение также применяется к любому типу класса, интерфейса, делегата или массива. В контексте, допускающем значение NULL, должен быть ссылочным типом, T не допускаемым значением NULL. |
where T : class? |
Аргумент типа должен быть ссылочным типом, допускающим значения NULL или не допускающим значения NULL. Это ограничение применяется также к любому классу, интерфейсу, делегату или типу массива, включая записи. |
where T : notnull |
Аргумент типа должен быть типом, не допускающим значения NULL. Аргумент может быть ненулевым ссылочным типом или типом значения, не допускающего значение 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 : <имя> интерфейса? |
Аргумент типа должен являться заданным интерфейсом или реализовывать его. Можно указать несколько ограничений интерфейса. Заданный в ограничении интерфейс также может быть универсальным. В контексте, допускающем значение NULL, может быть ссылочным типом, T не допускаемым значением NULL, или типом значения. 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
, иnotnull
class
class?
unmanaged
,. Если вы предоставляете любой из этих ограничений, это должно быть первое ограничение, указанное для этого параметра типа. - Ограничение базового класса (
where T : Base
или) не может сочетаться с какими-либо ограничениямиstruct
, ,class
илиclass?
notnull
unmanaged
.where T : Base?
- Можно применить не более одного ограничения базового класса в любой форме. Если вы хотите поддерживать базовый тип, допускающий значение NULL, используйте
Base?
. - Нельзя назвать как ненулевой, так и null-форму интерфейса в качестве ограничения.
- Ограничение
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 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;
}
}
Это ограничение позволяет универсальному классу использовать свойство Employee.Name
. Ограничение указывает, что все элементы типа T
гарантированно являются либо объектом Employee
, либо объектом, который наследует от Employee
.
К одному параметру типа можно применять несколько ограничений, которые сами по себе могут быть универсальными типами, как показано ниже:
class EmployeeList<T> where T : notnull, Employee, IComparable<T>, new()
{
// ...
public void AddDefault()
{
T t = new T();
// ...
}
}
При применении where T : class
ограничения избегайте ==
операторов и !=
параметров типа, так как эти операторы проверяют только эталонное удостоверение, а не на равенство значений. Такое поведение будет наблюдаться даже в том случае, если эти операторы будут перегружены в типе, используемом в качестве аргумента. Эта особенность показана в следующем коде, который будет возвращать значение false даже в том случае, если класс String перегружает оператор ==
.
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);
}
Компилятору известно только то, что 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
позволяет написать код, который работает с делегатами типобезопасным образом. Следующий код определяет метод расширения, который объединяет два делегата при условии, что они одного типа:
public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> 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
. В следующем примере выполняется поиск всех допустимых значений для типа перечисления и создается словарь, который сопоставляет эти значения с их строковым представлением.
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
и 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);
}
Предыдущий синтаксис требует, чтобы разработчики использовали явную реализацию интерфейса для этих методов. Предоставление дополнительного ограничения позволяет интерфейсу определять операторы с точки зрения параметров типа. Типы, реализующие интерфейс, могут неявно реализовать методы интерфейса.
Разрешает структуру ссылок
Анти-ограничение 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
.