Speak.Me Учить иностранные слова

C#: обобщения (Generics)

В C# существует два механизма повторного использования кода в разных типах: наследование и обобщения (generics). Наследование делает возможным повторное использование благодаря применению базового класса, а обобщения — благодаря использованию параметров типа — шаблонов, или своеобразных плейсхолдеров типов.

Обобщенные типы (Generic Types)

Обобщенный тип содержит, или точнее объявляет параметры типа (type parameters) — своеобразные плейсхолдеры типов, которые будут заполнены в дальнейшем при использовании обобщенного типа, путем передачи в него аргументов типа (type arguments).

В примере объявляется обобщенный тип Stack<T>, который будет хранить экземпляры типа TStack<T> объявляет единственный параметр типа — T. Использовать Stack<T> можно так:

Stack<int> заполняет параметр типа T аргументом типа int. Фактически Stack<int> имеет следующее определение:

Замену параметра типа T на конкретный тип при инициализации экземпляра обобщенного типа называют закрытием типа. Так же говорят, что Stack<T> является открытым типом (open type), а Stack<int>закрытым типом (closed type). Создать экземпляр открытого типа нельзя — все параметры типа должны быть закрыты при инициализации.

Задачи, которые решаю обобщенные типы, можно было бы решить с помощью базового типа object (что и делалось до появления в C# обобщений). Но такое решение является не самым эффективным, поскольку при выполнении программы придется выполнять по несколько операций упаковки и распаковки значений (boxing — приведение к объектному типу и unboxing — восстановление значения), что негативно сказывается на производительности и создает риск возникновения во время выполнения ошибок в приведении типов. Использование обобщений лишено этих недостатков.

Обобщенные методы (Generic Methods)

Параметры типа могут задаваться не только для типа в целом, но и для отдельного метода. Обобщенный метод объявляет параметры типа в сигнатуре метода.

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

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

В пределах обобщенного типа методы по умолчанию не являются обобщенными. Чтобы они стали таковыми им нужно явно задать параметр типа (в треугольных скобках).

Методы и типы — единственные конструкции, которые могут быть обобщенными, т.е. вводить параметры типа. Свойства, индексаторы, события, поля, конструкторы, операторы и т.д. не могут объявлять параметры типа, однако они могут использовать любые параметры типа уже объявленные в их родительском типе. Например, обобщенный тип Stack<T> может быть дополнен индексатором:

Объявление параметров типа

Параметры типа могут вводиться при объявлении классов, структур, интерфейсов, делегатов и методов. Обобщенный тип или метод может иметь несколько параметров типа:

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

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

Оператор typeof и свободные обобщенные типы

Открытые обобщенные типы не существуют во время выполнения: они закрываются при компиляции. Однако свободный (unbound) обобщенный тип может существовать во время выполнения, но исключительно как объект Type. Чтобы объявить свободный обобщенный тип нужно использовать оператор typeof:

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

А также открытого:

Оператор default и обобщенное значение по умолчанию

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

Ограничения обобщений (Generic Constraints)

По умолчанию, параметр типа может быть замещен абсолютно любым типом. Но к параметру типа можно применить ограничения (constraints), которые будут требовать более конкретных аргументов типов. Существует 6 видов ограничений:

  • where T : {класс} ограничение базовым классом: параметр типа может быть либо указанным классом, либо его производным классом (допустимо если экземпляр параметра типа может быть автоматически приведен к базовому классу)
  • where T : {интерфейс} ограничение интерфейсом: параметр типа может быть только реализацией указанного интерфейса (либо может быть автоматически приведен к этому интерфейсу)
  • where T : class — ограничение ссылочным типом: параметр типа может быть ссылочным типом
  • where T : struct — ограничение значимым типом: параметр типа может быть не нулевым значимым типом
  • where T : new() — ограничение конструктором без параметров: параметр типа должен иметь публичный (public) конструктор без параметров и позволять вызвать new()

  • where U : T — ограничение конкретным типом: параметр типа должен совпадать с типом другого параметра, или быть его производным

Ограничения могут применяться как к обобщенным методам, так и к обобщенным типам.

Наследование обобщенных типов

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

Ссылка на самого себя

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

Статические поля

Статические поля всегда уникальны для конкретного закрытого типа:

Ковариантность (covariance), контравариантность (contravariance) и инвариантность (Invariance)

Ковариантностью называется сохранение иерархии наследования типов, передаваемых в качестве аргументов типа, в обобщенных типах в том же порядке. Так, если класс Cat наследует от класса Animal, то естественно полагать, что обобщенный тип IEnumerable<Cat> будет потомком перечисления IEnumerable<Animal>. Действительно, «список из пяти кошек» — это частный случай «списка из пяти животных». В таком случае говорят, что тип (в данном случае обобщённый интерфейс) IEnumerable<T> ковариантен своему параметру типа T.

Чтобы сделать обобщенный тип ковариантным нужно перед его параметром типа добавить модификатор out.  Такой тип можно будет только возвращать из метода.

Ковариантный обобщенный метод  (или делегат) не может принимать в качестве параметра экземпляр своего ковариантного типа — это приведет к ошибке компиляции:

Примерами ковариантных обобщенных типов являются интерфейсы IEnumerator<T> и IEnumerable<T>, поэтому можно, например, привести IEnumerable<string> к типу IEnumerable<object>.

Ковариантность возможна только для приведения ссылочных типов, но не применима для приведения к объектному типу значимых типов. К примеру, если метод принимает параметр типа IPoppable<object>, мы можем передать ему при вызове IPoppable<string>, но не можем передать IPoppable<int>.

Контравариантностью называется замену иерархии наследования типов, передаваемых в качестве аргументов типа, на противоположную в обобщенных типах. Так, если класс String наследует от класса Object, а делегат Action<T> определён как метод, принимающий объект типа T, то Action<Object> наследует от делегата Action<String>, а не наоборот. Действительно, если все строки — объекты, то всякий метод, оперирующий произвольными объектами, может выполнить операцию над строкой, но не наоборот. В таком случае говорят, что тип (в данном случае обобщённый делегат) Action<T> контравариантен своему параметру типу T.

Контравариантность в С# возможна только для интерфейсов и делегатов, в которые параметр типа передается с модификатором in, т.е. на входящей позиции. Возвращаясь к классу Stack<T>, предположим, что теперь он реализует такой интерфейс:

Тогда мы можем сделать следующее:

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

Отсутствие наследования между производными типами называется инвариантностью. По умолчанию все типы инвариантны.

Обобщенный тип может совмещать в себе ковариантность и контравариантность для разных параметров.