Ограничения параметров типа (руководство по программированию на C#)

Ограничения сообщают компилятору о характеристиках, которые должен иметь аргумент типа. Без ограничений аргумент типа может быть любым типом. Компилятор может только предполагать членов System.Object, который является главным базовым классом для всех типов .NET. Дополнительные сведения см. в статье Зачем использовать ограничения. Если клиентский код использует тип, который не удовлетворяет ограничению, компилятор выдает ошибку. Ограничения задаются с помощью контекстного ключевого слова where. В следующей таблице описываются различные типы ограничений:

Ограничение Описание
where T : struct Аргумент типа должен быть типом значения, не допускающим значения NULL. См. дополнительные сведения о типах значений, допускающих значение NULL. Поскольку все типы значений имеют доступный конструктор без параметров, ограничение struct подразумевает наличие ограничения new() и не может применяться в сочетании с ограничением new(). Ограничение struct нельзя использовать вместе с ограничением unmanaged.
where T : class Аргумент типа должен быть ссылочным типом. Это ограничение также применяется к любому типу класса, интерфейса, делегата или массива. В контексте, допускающем значение NULL, должен быть ссылочным типом, T не допускаемым значением NULL.
where T : class? Аргумент типа должен быть ссылочным типом, допускающим значения NULL или не допускающим значения NULL. Это ограничение также применяется к любому типу класса, интерфейса, делегата или массива.
where T : notnull Аргумент типа должен быть типом, не допускающим значения NULL. Аргумент может быть ссылочным типом, не допускаемым значением NULL, или типом значения, не допускающего значение NULL.
where T : default Это ограничение устраняет неоднозначность, если необходимо указать неограниченный параметр типа, переопределяя метод или указывая явную реализацию интерфейса. Ограничение default подразумевает базовый метод без ограничения class или struct. Дополнительные сведения см. в разделе характеристик ограничения default.
where T : unmanaged Аргумент типа должен быть неуправляемым типом, не допускающим значения NULL. Ограничение unmanaged подразумевает ограничение struct и не может использоваться совместно с ограничением struct или new().
where T : new() Аргумент типа должен иметь общий конструктор без параметров. При одновременном использовании нескольких ограничений последним должно указываться ограничение new(). Ограничение new() не может использоваться с ограничениями struct и unmanaged.
where T :<имя базового класса> Аргумент типа должен иметь базовый класс или производный от него класс. В контексте, допускающем значение NULL, должен быть ссылочным типом, не допускаемым значением NULL, T производным от указанного базового класса.
where T :<Имя> базового класса? Аргумент типа должен иметь базовый класс или производный от него класс. В контексте, допускающем значение NULL, может быть либо типом, допускаемым null, T либо не допускаемым null, производным от указанного базового класса.
where T :<имя интерфейса> Аргумент типа должен являться заданным интерфейсом или реализовывать его. Можно указать несколько ограничений интерфейса. Заданный в ограничении интерфейс также может быть универсальным. В контексте, допускающем значение NULL, должен быть типом, T не допускающим значение NULL, который реализует указанный интерфейс.
where T :<имя> интерфейса? Аргумент типа должен являться заданным интерфейсом или реализовывать его. Можно указать несколько ограничений интерфейса. Заданный в ограничении интерфейс также может быть универсальным. В контексте, допускающем значение NULL, может быть ссылочным типом, T не допускаемым значением NULL, или типом значения. T не может быть типом значения, допускающим значения NULL.
where T : U Аргумент типа, указанный для T, должен быть аргументом, указанным для U, или производным от него. Если в контексте, допускающем значения NULL, U является ссылочным типом, допускающим значения NULL, T должен быть ссылочным типом, не допускающим значения NULL. Если U является ссылочным типом, допускающим значения NULL, T может допускать или не допускать значения NULL.

Зачем использовать ограничения

Ограничения определяют возможности и ожидания параметра типа. Объявление этих ограничений означает, что можно использовать операции и вызовы методов ограничивающего типа. Если универсальный класс или метод использует любые другие операции с универсальными элементами, помимо простого присвоения или вызова методов, которые не поддерживает 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 : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

При применении ограничения 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? можно использовать либо с ограничением structclass , но один из них должен присутствовать. Если используется ограничение class, T? ссылается на ссылочный тип, допускающий значения NULL, для T. Начиная с C# 9 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.MulticastDelegate в System.Delegate качестве ограничения базового класса. В среде 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>
{
    public abstract static T operator +(T left, T right);
    public abstract static T operator -(T left, T right);
}

Этот шаблон позволяет компилятору C# определить содержащий тип для перегруженных операторов или любого static virtual метода.static abstract Он предоставляет синтаксис, чтобы операторы сложения и вычитания можно было определить для содержащего типа. Без этого ограничения параметры и аргументы должны быть объявлены как интерфейс, а не параметр типа:

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

Приведенный выше синтаксис потребует от средств реализации использовать явную реализацию интерфейса для этих методов. Предоставление дополнительного ограничения позволяет интерфейсу определять операторы с точки зрения параметров типа. Типы, реализующие интерфейс, могут неявно реализовать методы интерфейса.

См. также раздел