Содержание
В 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, например, в качестве возвращаемого значения.
Отсутствие наследования между производными типами называется инвариантностью. По умолчанию все типы инвариантны.
Обобщенный тип может совмещать в себе ковариантность и контравариантность для разных параметров.

