Зачем они нужны?
Пусть у нас есть несколько классов животных: базовое животное BaseAnimal, черепаха Turtle, собака Dog. Если вы не создали их в предыдущих статьях, ничего страшного - просто создайте пустые классы. Черепаха и собака наследуются от базового животного.
Пусть мы хотим сделать коробки для животных. Коробка - это такой класс, который должен работать так:
То есть, при создании коробки черепаха помещается внутрь, а при вызове метода Get() выдаётся наружу.
Реализация коробки проста:
Получив черепаху на вход конструктора, коробка запоминает её в приватном поле, чтобы затем выдать наружу.
Аналогично выглядит коробка для собаки:
Но сейчас нам придётся делать коробки под каждый вид животного, а их может быть много. Вообще, дублирование кода - это нехорошо. Как быть?
Попробуем создать коробку для базового животного. В неё можно будет положить любое животное, так как все они наследуются от базового:
Но, чтобы в неё можно было положить любое животное, тип переменной _animal должен быть BaseAnimal. Но тогда метод Get() должен выдавать тоже BaseAnimal. Таким образом, на стороне-получателе все доставаемые животные будут иметь тип BaseAnimal, а не Turtle или Dog. Сравните работу общей коробки и коробки для черепахи:
Niki поместили в черепашью коробку, и при доставании оттуда unboxedNiki имеет тип Turtle - всё хорошо. А Vova поместили в общую коробку для всех животных, и при доставании оттуда он имеет тип только BaseAnimal. Да, по факту Vova остаётся черепахой, но вызвать черепаший метод без приведения типов будет нельзя:
А приведение типов - это вещь неудобная. Как минимум потому, что ошибки приведения типов обнаруживаются только во время работы программы - заранее при компиляции увидеть их нельзя. Например, положим собаку в общую для животных коробку. При доставании оттуда по синтаксису она будет базовым животным. И при приведении собаки к черепахе будет ошибка во время работы программы:
Можно проверять if (unboxedSharik is Turtle) ..., но когда программа будет утопать в таких проверках, это будет похоже на затычку, а не на решение проблемы.
Как бы сделать так, чтобы коробка запоминала тип данного ей животного и автоматически выдавала обратно животное того же типа? Вот тут нам и помогут
Универсальные (обобщённые) типы
Создадим класс UniversalBox ("Универсальная коробка"), взяв за основу коробку для черепахи. Рассмотрим её ещё раз:
Она отличается от коробки для собак только типом данных - Turtle вместо Dog. В таких случаях мы можем завести особую переменную, хранящую тип данных. Она называется универсальный параметр. По стилю кода, универсальные параметры должны начинаться с T. Обозначим нашу переменную TAnimal:
Это не обычная переменная. Скорее, это параметр типа. Мы параметризуем наш класс и в результате получаем как бы набор классов с разными типами данных.
Теперь поменяем тип данных Turtle на TAnimal во всех местах его использования:
Теперь переменная _animal имеет тип TAnimal. Этот же тип подаётся на вход конструктора и получается на выходе метода Get().
Повторюсь, наш класс параметризован параметром TAnimal. Для ясности можно считать, что при разных значениях TAnimal будут получаться разные классы. В C++ всё так и работает; для каждого значения параметра при компиляции создаётся отдельный класс. В C# всё устроено по-другому, но смысл такой же.
Значение универсального параметра должно быть понятно в момент создания объекта. Поэтому мы указываем его при вызове конструктора:
У первой универсальной коробки был указан тип Turtle, соответственно, переменная _animal и результат метода Get() для неё имеют тип Turtle.
У второй универсальной коробки был указан тип Dog, соответственно, переменная _animal и результат метода Get() для неё имеют тип Dog.
Ограничения на универсальные типы
Сейчас тип TAnimal животного в коробке может быть любым - даже и число, и строка. Но если нам понадобится обратиться к полям/методам животного, то мы не сможем этого сделать, потому что нет гарантии, что это животное. Можно наложить ограничение на универсальный тип, что он обязательно наследуется от базового животного:
Теперь мы можем обратиться к полю Name животного, потому что оно есть у BaseAnimal.
На один универсальный параметр можно наложить сразу несколько ограничений. Например, потребуем, чтобы TAnimal наследовался от BaseAnimal и имел конструктор без параметров:
Более осмысленно это может быть в классе "Генератор":
Использование генератора может выглядеть так:
На каждый вызов метода Generate() создаётся новый объект типа T. В нашем случае - целое число. Но можно заставить генератор генерировать генераторы:
Результат в консоли:
Универсальные типы на уровне метода
Если универсальные типы на уровне класса определяются в момент создания объекта и запоминаются на каждый объект, то универсальные типы на уровне метода определяются в момент его вызова и запоминаются на время работы метода. Например, генератору необязательно помнить, какой тип данных он генерирует - этот тип данных можно определять каждый раз при вызове метода Generate():
Теперь один и тот же генератор может генерировать объекты разных типов данных:
Результат в консоли:
Далее
Мы можем использовать несколько универсальных типов в одном классе - для этого объявляем их в уголках через запятую: MyClass<T1, T2, T3>. Более сложные примеры универсальных типов будут далее.
Пишем имитацию сервиса для веб-приложения:
Оглавление: