Ограничения параметров типа (руководство по программированию на 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.

Некоторые ограничения являются взаимоисключающими, и некоторые ограничения должны быть в указанном порядке:

  • Вы можете применить не более одного из ограничений struct, и notnullclassclass?unmanaged ,. Если вы предоставляете любой из этих ограничений, это должно быть первое ограничение, указанное для этого параметра типа.
  • Ограничение базового класса (where T : Baseили) не может быть объединено с какими-либо ограничениямиstruct, , classили class?notnullunmanaged.where T : Base?
  • Можно применить не более одного ограничения базового класса в любой форме. Если вы хотите поддерживать базовый тип, допускающий значение NULL, используйте Base?.
  • Нельзя назвать как ненулевой, так и null-форму интерфейса в качестве ограничения.
  • Ограничение new() нельзя использовать с ограничением struct или unmanaged. Если указать new() ограничение, это должно быть последнее ограничение для этого параметра типа.
  • Ограничение default может применяться только к переопределениям или явным реализациям интерфейса. Его нельзя объединить с struct ограничениями или class ограничениями.

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

Ограничения определяют возможности и ожидания параметра типа. Объявление этих ограничений означает, что можно использовать операции и вызовы методов ограничивающего типа. Ограничения применяются к параметру типа, когда универсальный класс или метод использует любую операцию с универсальными элементами за пределами простого назначения, которая включает вызов любых методов, не поддерживаемых 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, System.Collections.Generic.IList<T>, IDisposable, 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. 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>
{
    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);
}

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

См. также