Содержание
- Обобщенные типы (Generic Types)
- Обобщенные методы (Generic Methods)
- Объявление параметров типа
- Оператор typeof и свободные обобщенные типы
- Оператор default и обобщенное значение по умолчанию
- Ограничения обобщений (Generic Constraints)
- Наследование обобщенных типов
- Ссылка на самого себя
- Статические поля
- Ковариантность (covariance), контравариантность (contravariance) и инвариантность (Invariance)
В C# существует два механизма повторного использования кода в разных типах: наследование и обобщения (generics). Наследование делает возможным повторное использование благодаря применению базового класса, а обобщения — благодаря использованию параметров типа — шаблонов, или своеобразных плейсхолдеров типов.
Обобщенные типы (Generic Types)
Обобщенный тип содержит, или точнее объявляет параметры типа (type parameters) — своеобразные плейсхолдеры типов, которые будут заполнены в дальнейшем при использовании обобщенного типа, путем передачи в него аргументов типа (type arguments).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Stack<T> { int position; T[] data = new T[100]; public void Push(T obj) { data[position++] = obj; } public T Pop() { return data[--position]; } } |
В примере объявляется обобщенный тип Stack<T>
, который будет хранить экземпляры типа T
. Stack<T>
объявляет единственный параметр типа — T
. Использовать Stack<T>
можно так:
1 2 3 4 5 6 7 | Stack<int> stack = new Stack<int>(); stack.Push(5); stack.Push(10); int x = stack.Pop(); // x = 10 int y = stack.Pop(); // y = 5 |
Stack<int>
заполняет параметр типа T
аргументом типа int
. Фактически Stack<int>
имеет следующее определение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class ### { int position; int[] data; public void Push(int obj) { data[position++] = obj; } public int Pop() { return data[--position]; } } |
Замену параметра типа T
на конкретный тип при инициализации экземпляра обобщенного типа называют закрытием типа. Так же говорят, что Stack<T>
является открытым типом (open type), а Stack<int>
— закрытым типом (closed type). Создать экземпляр открытого типа нельзя — все параметры типа должны быть закрыты при инициализации.
Задачи, которые решаю обобщенные типы, можно было бы решить с помощью базового типа object
(что и делалось до появления в C# обобщений). Но такое решение является не самым эффективным, поскольку при выполнении программы придется выполнять по несколько операций упаковки и распаковки значений (boxing — приведение к объектному типу и unboxing — восстановление значения), что негативно сказывается на производительности и создает риск возникновения во время выполнения ошибок в приведении типов. Использование обобщений лишено этих недостатков.
Обобщенные методы (Generic Methods)
Параметры типа могут задаваться не только для типа в целом, но и для отдельного метода. Обобщенный метод объявляет параметры типа в сигнатуре метода.
1 2 3 4 | static void Swap<T> (ref T a, ref T b) { T temp = a; a = b; b = temp; } |
Данный метод меняет значения двух переменных местами. Применить его можно так:
1 2 | int x = 5, y = 10; Swap (ref x, ref y); |
Обычно нет необходимости передавать в обобщенный метод аргументы типа — компилятор сам определит тип. В случае неоднозначности, обобщенный метод может быть вызван с аргументами типа:
1 | Swap<int> (ref x, ref y); |
В пределах обобщенного типа методы по умолчанию не являются обобщенными. Чтобы они стали таковыми им нужно явно задать параметр типа (в треугольных скобках).
Методы и типы — единственные конструкции, которые могут быть обобщенными, т.е. вводить параметры типа. Свойства, индексаторы, события, поля, конструкторы, операторы и т.д. не могут объявлять параметры типа, однако они могут использовать любые параметры типа уже объявленные в их родительском типе. Например, обобщенный тип Stack<T>
может быть дополнен индексатором:
1 | public T this [int index] { get { return data[index]; } } |
Объявление параметров типа
Параметры типа могут вводиться при объявлении классов, структур, интерфейсов, делегатов и методов. Обобщенный тип или метод может иметь несколько параметров типа:
1 2 3 | class Dictionary<TKey, TValue> {...} var myDic = new Dictionary<int,string>(); |
Обобщенные типы и методы могут быть перегружены, для этого им нужно задавать разное количество параметров типа:
1 2 3 | class A<T> {} class A<T1,T2> {} |
По соглашению, у обобщенных методов и типов с единственным параметром типа этот параметр называется просто T
, а если параметров много, каждый из них должен иметь более содержательное название с префиксом T
.
Оператор typeof и свободные обобщенные типы
Открытые обобщенные типы не существуют во время выполнения: они закрываются при компиляции. Однако свободный (unbound) обобщенный тип может существовать во время выполнения, но исключительно как объект Type
. Чтобы объявить свободный обобщенный тип нужно использовать оператор typeof
:
1 2 3 4 5 6 7 8 9 | class A<T> {} class A<T1,T2> {} Type a1 = typeof (A<>); // Свободный тип Type a2 = typeof (A<,>); // 2 аргумента типа Console.Write (a2.GetGenericArguments().Count()); // 2 |
Оператор typeof
можно использовать и для объявления закрытого типа:
1 | Type a3 = typeof (A<int,int>); |
А также открытого:
1 | class B<T> { void X() { Type t = typeof (T); } } |
Оператор default и обобщенное значение по умолчанию
С помощью ключевого слова default
можно получить значение по умолчанию для параметра типа. Значением по умолчанию для ссылочных типов будет null
, а для значимых типов — результат побитового обнуления полей типа.
1 2 3 4 5 6 | static void Zap<T> (T[] array) { for (int i = 0; i < array.Length; i++) array[i] = default(T); } |
Ограничения обобщений (Generic Constraints)
По умолчанию, параметр типа может быть замещен абсолютно любым типом. Но к параметру типа можно применить ограничения (constraints), которые будут требовать более конкретных аргументов типов. Существует 6 видов ограничений:
where T : {класс}
— ограничение базовым классом: параметр типа может быть либо указанным классом, либо его производным классом (допустимо если экземпляр параметра типа может быть автоматически приведен к базовому классу)123456class SomeClass {}class GenericClass<T,U,B> where T : SomeClass{...}where T : {интерфейс}
— ограничение интерфейсом: параметр типа может быть только реализацией указанного интерфейса (либо может быть автоматически приведен к этому интерфейсу)123456interface SomeInterface {}class GenericClass<T,U,B> where T : SomeInterface{...}where T : class
— ограничение ссылочным типом: параметр типа может быть ссылочным типом1234class GenericClass<T,U,B> where T : class{...}where T : struct
— ограничение значимым типом: параметр типа может быть не нулевым значимым типом1234class GenericClass<T,U,B> where T : struct{...}where T : new()
— ограничение конструктором без параметров: параметр типа должен иметь публичный (public) конструктор без параметров и позволять вызватьnew()
1234class GenericClass<T,U,B> where T : new(){...}where U : T
— ограничение конкретным типом: параметр типа должен совпадать с типом другого параметра, или быть его производным1234class GenericClass<T,U,B> where T : U{...}
Ограничения могут применяться как к обобщенным методам, так и к обобщенным типам.
Наследование обобщенных типов
Обобщенный класс может наследоваться от другого обобщенного класса. При этом производный класс может оставлять параметр типа базового класса открытым, или закрывать его конкретным типом, а также добавлять свои параметры типа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Stack<T> { ... } class SpecialStack<T> : Stack<T> // Оставляет параметр типа открытым { ... } class IntStack : Stack<int> // Закрывает параметр типа типом int { ... } class KeyedList<T,TKey> : List<T> // Добавляет новый параметр типа { ... } |
Ссылка на самого себя
В качестве аргумента типа при закрытии обобщенного типа может использоваться сам обобщенный тип:
1 2 3 4 5 6 7 8 9 10 11 12 | public interface IEquatable<T> { bool Equals (T obj); } public class Balloon : IEquatable<Balloon> { public bool Equals (Balloon b) { ... } } |
Статические поля
Статические поля всегда уникальны для конкретного закрытого типа:
1 2 3 4 5 6 7 8 9 10 11 12 | class Bob<T> { public static int Count; } Console.WriteLine (++Bob<int>.Count); // 1 Console.WriteLine (++Bob<int>.Count); // 2 Console.WriteLine (++Bob<string>.Count); // 1 Console.WriteLine (++Bob<object>.Count); // 1 |
Ковариантность (covariance), контравариантность (contravariance) и инвариантность (Invariance)
Ковариантностью называется сохранение иерархии наследования типов, передаваемых в качестве аргументов типа, в обобщенных типах в том же порядке. Так, если класс Cat
наследует от класса Animal
, то естественно полагать, что обобщенный тип IEnumerable<Cat>
будет потомком перечисления IEnumerable<Animal>
. Действительно, «список из пяти кошек» — это частный случай «списка из пяти животных». В таком случае говорят, что тип (в данном случае обобщённый интерфейс) IEnumerable<T>
ковариантен своему параметру типа T
.
Чтобы сделать обобщенный тип ковариантным нужно перед его параметром типа добавить модификатор out
. Такой тип можно будет только возвращать из метода.
1 2 3 4 | public interface IPoppable<out T> { T Pop(); } |
Ковариантный обобщенный метод (или делегат) не может принимать в качестве параметра экземпляр своего ковариантного типа — это приведет к ошибке компиляции:
1 | static void Foo<out T> (T a) // Ошибка компиляции |
Примерами ковариантных обобщенных типов являются интерфейсы 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>
, предположим, что теперь он реализует такой интерфейс:
1 | public interface IPushable<in T> { void Push (T obj); } |
Тогда мы можем сделать следующее:
1 2 3 | IPushable<Animal> animals = new Stack<Animal>(); IPushable<Bear> bears = animals; bears.Push (new Bear()); |
В противоположность ковариантности, компилятор выдаст ошибку если попытаться использовать контравариантный параметр типа на позиции out
, например, в качестве возвращаемого значения.
Отсутствие наследования между производными типами называется инвариантностью. По умолчанию все типы инвариантны.
Обобщенный тип может совмещать в себе ковариантность и контравариантность для разных параметров.