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

Что такое дженерики?

Обобщения — это способ определить класс, структуру, интерфейс или метод, при котором мы можем отложить спецификацию одного или нескольких используемых ими типов до тех пор, пока класс или метод не будет объявлен и создан клиентским кодом. Много раз нам нужно определить класс или метод (то есть поведение), который является общим для различных типов. В этих случаях вместо определения нескольких похожих классов или методов, имеющих одинаковое поведение, за исключением типа, с которым они работают, мы можем определить общий класс или метод. Затем мы можем использовать этот общий класс или метод с разными типами данных, которые разделяют поведение. Это помогает предотвратить дублирование кода, централизовать логику, упростить тестирование, устранить стоимость или риск приведения типов во время выполнения или операций упаковки. Это также делает ваш код более гибким и пригодным для повторного использования.

Когда и зачем использовать дженерики

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

Примеры дженериков

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

public class Stack<T>
{
    private List<T> items = new List<T>();

    public void Push(T item)
    {
        items.Add(item);
    }

    public T Pop()
    {
        T item = items[items.Count - 1];
        items.RemoveAt(items.Count - 1);
        return item;
    }
}

В этом примере параметр типа T используется для указания типа элементов в стеке. Когда вы создаете экземпляр класса Stack, вы указываете тип данных, который хотите использовать.

Ограничения

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

public class Test<T> where T : struct
{
}

В приведенном выше примере мы ограничили тип, который можно использовать с Test универсальным классом, равным struct.

К одному или нескольким параметрам типа можно добавить несколько ограничений:

public class Base 
{ 
}

public class Test<T, U>
    where U : struct
    where T : Base, new()
{ 
}

В приведенном выше примере:

  • Параметр типа `U` должен быть структурой
  • Параметр типа `T` должен иметь тип, производный от Base, и иметь открытый конструктор без параметров.

Ковариантность и контравариантность

Ковариантность и контравариантность — это расширенные функции универсальных шаблонов, которые позволяют использовать производный тип данных вместо базового типа данных или базовый тип данных вместо производного типа данных. Это может быть полезно в определенных сценариях, например при работе с коллекциями.

Вот пример ковариации:

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariance

В этом примере у нас есть список строк, которые мы присваиваем переменной типа IEnumerable.

Вот пример контравариантности:

Action<object> printObject = (object o) => Console.WriteLine(o);
Action<string> printString = printObject; // contravariance

В этом примере у нас есть метод, который принимает объект в качестве параметра и выводит его на консоль. Мы присваиваем этот метод переменной типа Action. Это возможно, потому что строка является производным типом объекта.

Заключение

  • Используйте универсальные типы, чтобы максимизировать повторное использование кода, безопасность типов и производительность.
  • Чаще всего дженерики используются для создания классов коллекций.
  • Вы можете создавать свои собственные универсальные интерфейсы, классы, методы, события и делегаты.
  • Общие классы могут быть ограничены, чтобы обеспечить доступ к методам для определенных типов данных.
  • Информацию о типах, которые используются в общем типе данных, можно получить во время выполнения с помощью отражения.

Первоначально опубликовано на https://amitlale.github.io 7 августа 2023 г.