Найти в Дзене
"Мы"-Прогер

Изучаем C# - Универсальные (обобщённые) типы

Пусть у нас есть несколько классов животных: базовое животное BaseAnimal, черепаха Turtle, собака Dog. Если вы не создали их в предыдущих статьях, ничего страшного - просто создайте пустые классы. Черепаха и собака наследуются от базового животного. Пусть мы хотим сделать коробки для животных. Коробка - это такой класс, который должен работать так: То есть, при создании коробки черепаха помещается внутрь, а при вызове метода Get() выдаётся наружу. Реализация коробки проста: Получив черепаху на вход конструктора, коробка запоминает её в приватном поле, чтобы затем выдать наружу. Аналогично выглядит коробка для собаки: Но сейчас нам придётся делать коробки под каждый вид животного, а их может быть много. Вообще, дублирование кода - это нехорошо. Как быть? Попробуем создать коробку для базового животного. В неё можно будет положить любое животное, так как все они наследуются от базового: Но, чтобы в неё можно было положить любое животное, тип переменной _animal должен быть BaseAnimal. Но
Оглавление

Зачем они нужны?

Пусть у нас есть несколько классов животных: базовое животное BaseAnimal, черепаха Turtle, собака Dog. Если вы не создали их в предыдущих статьях, ничего страшного - просто создайте пустые классы. Черепаха и собака наследуются от базового животного.

Пусть мы хотим сделать коробки для животных. Коробка - это такой класс, который должен работать так:

То есть, при создании коробки черепаха помещается внутрь, а при вызове метода Get() выдаётся наружу.

Реализация коробки проста:

-2

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

Аналогично выглядит коробка для собаки:

-3

Но сейчас нам придётся делать коробки под каждый вид животного, а их может быть много. Вообще, дублирование кода - это нехорошо. Как быть?

Попробуем создать коробку для базового животного. В неё можно будет положить любое животное, так как все они наследуются от базового:

-4

Но, чтобы в неё можно было положить любое животное, тип переменной _animal должен быть BaseAnimal. Но тогда метод Get() должен выдавать тоже BaseAnimal. Таким образом, на стороне-получателе все доставаемые животные будут иметь тип BaseAnimal, а не Turtle или Dog. Сравните работу общей коробки и коробки для черепахи:

-5

Niki поместили в черепашью коробку, и при доставании оттуда unboxedNiki имеет тип Turtle - всё хорошо. А Vova поместили в общую коробку для всех животных, и при доставании оттуда он имеет тип только BaseAnimal. Да, по факту Vova остаётся черепахой, но вызвать черепаший метод без приведения типов будет нельзя:

-6

А приведение типов - это вещь неудобная. Как минимум потому, что ошибки приведения типов обнаруживаются только во время работы программы - заранее при компиляции увидеть их нельзя. Например, положим собаку в общую для животных коробку. При доставании оттуда по синтаксису она будет базовым животным. И при приведении собаки к черепахе будет ошибка во время работы программы:

-7

Можно проверять if (unboxedSharik is Turtle) ..., но когда программа будет утопать в таких проверках, это будет похоже на затычку, а не на решение проблемы.

Как бы сделать так, чтобы коробка запоминала тип данного ей животного и автоматически выдавала обратно животное того же типа? Вот тут нам и помогут

Универсальные (обобщённые) типы

Создадим класс UniversalBox ("Универсальная коробка"), взяв за основу коробку для черепахи. Рассмотрим её ещё раз:

-8

Она отличается от коробки для собак только типом данных - Turtle вместо Dog. В таких случаях мы можем завести особую переменную, хранящую тип данных. Она называется универсальный параметр. По стилю кода, универсальные параметры должны начинаться с T. Обозначим нашу переменную TAnimal:

-9

Это не обычная переменная. Скорее, это параметр типа. Мы параметризуем наш класс и в результате получаем как бы набор классов с разными типами данных.

Теперь поменяем тип данных Turtle на TAnimal во всех местах его использования:

-10

Теперь переменная _animal имеет тип TAnimal. Этот же тип подаётся на вход конструктора и получается на выходе метода Get().

Повторюсь, наш класс параметризован параметром TAnimal. Для ясности можно считать, что при разных значениях TAnimal будут получаться разные классы. В C++ всё так и работает; для каждого значения параметра при компиляции создаётся отдельный класс. В C# всё устроено по-другому, но смысл такой же.

Значение универсального параметра должно быть понятно в момент создания объекта. Поэтому мы указываем его при вызове конструктора:

-11

У первой универсальной коробки был указан тип Turtle, соответственно, переменная _animal и результат метода Get() для неё имеют тип Turtle.

У второй универсальной коробки был указан тип Dog, соответственно, переменная _animal и результат метода Get() для неё имеют тип Dog.

Ограничения на универсальные типы

Сейчас тип TAnimal животного в коробке может быть любым - даже и число, и строка. Но если нам понадобится обратиться к полям/методам животного, то мы не сможем этого сделать, потому что нет гарантии, что это животное. Можно наложить ограничение на универсальный тип, что он обязательно наследуется от базового животного:

-12

Теперь мы можем обратиться к полю Name животного, потому что оно есть у BaseAnimal.

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

-13

Более осмысленно это может быть в классе "Генератор":

-14

Использование генератора может выглядеть так:

-15

На каждый вызов метода Generate() создаётся новый объект типа T. В нашем случае - целое число. Но можно заставить генератор генерировать генераторы:

-16

Результат в консоли:

-17

Универсальные типы на уровне метода

Если универсальные типы на уровне класса определяются в момент создания объекта и запоминаются на каждый объект, то универсальные типы на уровне метода определяются в момент его вызова и запоминаются на время работы метода. Например, генератору необязательно помнить, какой тип данных он генерирует - этот тип данных можно определять каждый раз при вызове метода Generate():

-18
-19

Теперь один и тот же генератор может генерировать объекты разных типов данных:

-20

Результат в консоли:

-21

Далее

Мы можем использовать несколько универсальных типов в одном классе - для этого объявляем их в уголках через запятую: MyClass<T1, T2, T3>. Более сложные примеры универсальных типов будут далее.

Пишем имитацию сервиса для веб-приложения:

Оглавление:

Изучаем C# с нуля - Очень краткий курс - Оглавление
"Мы"-Прогер27 января