Предисловие


Дорогой хабражитель, добро пожаловать в цикл статей, посвящённых применению Обобщений в языке C#.
Здесь мы рассмотрим их создание, использование, а также подводные камни, которые могут помешать нам сделать гибкие функции.


Вы в любой момент можете перейти к интересующей вас теме в оглавлении:



Что это и зачем оно нам?


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


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


int Clamp(int value, int min, int max) {
    if (value < min) { return min; }
    if (value > max) { return max; }
    return value;
}

Этот метод достаточно прост, но что, если нам понадобится так же работать и с типом float?
Безусловно, мы можем попросту дублировать данную функцию и заменить в ней все вхождения int на float.
Однако теперь у нас станет в два раза больше кода, и если понадобится что-то в нём поменять(например, сделать минимум закрытым(недостижимым)), то придётся вносить правки уже в 2 местах!


А что, если понадобится поддерживать ещё больше различных вариаций аргументов?


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


T Clamp<T>(T value, T min, T max) {
    if (value < min) { return min; }
    if (value > max) { return max; }
    return value;
}

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


Теперь тип T стал определяться во время компиляции на месте вызова метода. Но это также означает, что теперь мы имеем дело с любым доступным программе классом или структурой без каких-либо ограничений. Компилятор выдаст ошибку о невозможности сравнить между собой два объекта, ведь он не может быть уверенным в том, что они поддерживают нужную нам операцию. Для корректной работы функции придётся немного изменить код.


Ограничение типа


Зачастую нам требуется получить объект, обладающий определёнными свойствами. Например, в вышеупомянутой функции Clamp, передаваемый объект нужно сравнивать, а следовательно его класс должен реализовывать интерфейс IComparable.


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


T Clamp<T>(T value, T min, T max)
    where T: IComparable
{
    if (value < min) { return min; }
    if (value > max) { return max; }
    return value;
}

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


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


  • structтип значения(значимые типы, структуры), кроме позволяющих иметь нулевое(null) значение.
  • classссылочный тип(классы, делегаты и тому подобное).
  • new() — имеет публичный конструктор без параметров. Всегда обязан идти после остальных ограничителей.
  • [класс] — имеет тот же тип, что и базовый класс, или является производным от него.
  • [интерфейс] — является указанным интерфейсом или реализует его. Возможно указание множества интерфейсов.
  • [тип] — является тем же типом, что и указанный ранее(в качестве шаблонного типа) или является производным от него.

Несмотря на то, что объект позволяет сравнение, перегруженные операторы не могут быть использованы. Для правильной работы требуется использовать предоставляемый IComparable метод a.CompareTo(b). Он возвращает число меньше нуля при значении a меньшем, чем переданное для сравнения(b), или же больше нуля в противоположном случае. 0 возвращается при их равенстве.


Путём нехитрых преобразований получаем финальную версию нашего метода:


T Clamp<T>(T value, T min, T max)
    where T: IComparable
{
    if (value.CompareTo(min) < 0) { return min; }
    if (value.CompareTo(max) > 0) { return max; }
    return value;
}

Готово! Осталось её вызвать и сохранить результат.


Вызов универсальной функции


Для вызова шаблонной функции требуется указывать не только входные аргументы, но также и типы, которые будут подставлены в её тело.
Ниже приведён пример вызова Clamp с типом int:


int result = Clamp<int>(value, 0, 100);

Для указания шаблонного типа используется та же конструкция, что и при создании функции. После названия, в треугольных скобках, указываются требуемые классы/структуры/т.п, разделяемые запятыми.
В данном примере в переменную result будет записано число от 0 до 100 в зависимости от значения value.


Внимательные читатели могли заметить, что теперь в коде появились лишние указания типов, затрудняющие его восприятие. И они будут правы. В нашем случае компилятор может самостоятельно вычислить параметр T, используя знание о типах передаваемых аргументов. Следовательно, вызов может быть изменён следующим образом, без нарушения логики работы:


int result = Clamp(value, 0, 100);

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


Итог


В данной статье мы рассмотрели базовые случаи работы с шаблонами.


Если у вас остались какие-либо вопросы по поводу использования дженериков в C# — смело пишите комментарии, а я постараюсь на них ответить.

Комментарии (1)