Ограничения типов

Если предполагается, что универсальный класс или метод будут использовать любые другие операции с универсальными элементами, кроме обычного присвоения или вызывать методы, которые не поддерживает System.Object, то нам необходимо применить ограничения к параметру типа. Что из себя представляют ограничения типа и как используются в C# мы и рассмотрим сегодня.

Пример ограничений типа

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

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
}

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

            List<Employee> list = new List<Employee>();
            //тут каким-либо образом добавляем в список данные и работам сними
            ....
            //ищем сотрудников с именем Вася
            var result = list.Where(f=>f.Name == "Вася");
            //работаем с result
            ....

и такой способ вполне сработает так как мы указали, что в нашем списке 100% будут находиться объекты класса Employee или его потомки. Но что, если нам необходимо создать вот такой класс со списком сотрудников:

public class EmployeeList<T>
{
    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 FindByName(string name)
    { 
        //тут необходимо провести поиск по полю Name
    }
}

При разработке такого класса списка вам не удастся написать метод поиска по имени (FindByName) потому что, мы-то с вами понимаем, что хотим хранить в этом списке только объекты класса Employee или его наследники, а вот компилятор C# этого не знает и под типом T для компилятора может скрываться, что угодно — структуры, классы, строки. Например, следующий код не вызовет никаких ошибок:

EmployeeList<Employee> list = new EmployeeList<Employee>();
list.AddHead(new Employee { Id = 1, Name = "Вася", Position = "Сотрудник" });
list.AddHead(new Employee { Id = 1, Name = "Петя", Position = "Сотрудник" });
list.AddHead(new Employee { Id = 1, Name = "Коля", Position = "Сотрудник" });


EmployeeList<string> strings= new EmployeeList<string>();
strings.AddHead("Вася");
strings.AddHead("Петя");
strings.AddHead("Коля");

И вот здесь нам и пригодятся ограничения типов. Перепишем наш класс списка следующим образом:

public class EmployeeList<T> where T : Employee
{
           ///Тут описание свойств и методов класса из предыдущего примера реализации

 	public T FindByName(string name)
    {
        foreach (T t in this)
        {
            if (t.Name == name)
                return t;
        }
        return null;
    }
}

Обратите внимание на первую строку. Здесь мы указали ограничение типа (where T : Employee), то есть, указали компилятору, что в качестве T у этого класса будут выступать объекты класса Employee или его наследники. Благодаря этому ограничению мы смогли свободно воспользоваться свойством Name класса и создать метод поиска по имени. Если же мы теперь попытаемся создать список

EmployeeList<string> strings= new EmployeeList<string>();

то ещё до компиляции получим ошибку следующего содержания:

Ошибка CS0311 Тип «string» не может быть использован как параметр типа «T» в универсальном типе или методе «EmployeeList». Нет преобразования неявной ссылки из «string» в «ConsoleApp1.Employee».

При этом, следующий код сработает без ошибок:

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Position { get; set; }
    }

    public class Chief : Employee
    {
        public int Age { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            EmployeeList<Employee> list = new EmployeeList<Employee>();
            list.AddHead(new Employee { Id = 1, Name = "Вася", Position = "Сотрудник" });

            list.AddHead(new Chief { Id = 1, Name = "Коля", Position = "Шеф", Age = 40 });

        }
    }
}

Типы ограничений и стандартные ограничения

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

where T : тип_ограничения

где where — это ключевое слово, T — тип к которому применяется ограничение, тип_ограничения — само ограничение, например, как в нашем случае — конкретный тип, которому должен соответствовать ограничиваемый тип T.

В качестве стандартных ограничений наиболее часто используются следующие ограничения:

Ограничение Описание
where T: notnull T не должен быть NULL. T быть ссылочным типом, не допускаемым значением NULL, или типом значения, не допускающим значение NULL.
where T : struct Универсальный тип T должен быть типом значения, не допускающим значения NULL. При таком ограничении:

EmployeeList<int> list = new EmployeeList<int>(); //сработает без ошибок
EmployeeList<Employee> list = new EmployeeList<Employee>(); //ошибка: Для использования в качестве параметра "T" в универсальном типе или методе "EmployeeList<T>" тип "Employee" должен быть типом значения, не допускающим значения Null
where T : class Универсальный тип T должен быть ссылочным типом. Это ограничение также применяется к любому типу класса, интерфейса, делегата или массива.
where T : new() Универсальный тип T должен иметь общий конструктор без параметров. При одновременном использовании нескольких ограничений последним должно указываться ограничение new().
where T :<имя базового класса> Универсальный тип T должен иметь базовый класс или производный от него класс (см. наш пример выше).
where T :<имя интерфейса> Универсальный тип T должен являться заданным интерфейсом или реализовывать его. Можно указать несколько ограничений интерфейса.
where T : U Универсальный тип T, указанный для T, должен быть аргументом, указанным для U, или производным от него.

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

public class Person<T> where T : Employee, new()
    {
        
    }

если в качестве T будет использоваться Employee или любой производный от него тип, то этот тип должен иметь общий конструктор без параметров.

Если необходимо указать ограничения для нескольких универсальных типов, то для каждого из них используется конструкция where:

class GenericList<T, U>
where T : Employee, IComparable, new()
where U : struct
{

}

здесь мы указали ограничения для каждого универсального типа и, при этом, для типа T указали, что он должен быть типом Employee или его наследником и при этом реализовывать интерфейс IComparable.

Итого

Ограничения типов позволяют указать компилятору то, какими характеристиками должен обладать универсальный тип. Ограничения можно применять как к универсальным типам, так и к универсальным методам. Ограничения стоит применять, если предполагается, что универсальный класс или метод будут использовать любые другие операции с универсальными элементами, кроме обычного присвоения или вызывать методы, которые не поддерживает System.Object.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии