Есть множество вещей в программировании, которые мы интуитивно понимаем и используем. Так, понятия, о которых пойдёт речь часто касаются нас во время программирования. Мы поговорим о типах и о их преобразованиях. Страшные слова в следующих абзацах пришли из теории категорий, однако мы — программисты, а не математики, поэтому будем рассматривать всё это в контексте реальных языков программирования.
С места в карьер. Инвариантность типов обобщений
Начнём с простого примера, который иногда вызывает недоумение у новичков. Так, если мы можем привести ссылку на String к ссылке на Object, то почему не проходит следующее (C#):
List<object> l = new List<string>(); // Error!
Всё дело в том, что List<object> и List<string> никак не связаны в иерархии наследования, несмотря на то, что String — дочерний класс Object. Такое поведение является инвариантным, так, типы обобщений в С# – инвариантны. Это решение разумно. Оно позволяет оградить нас от ошибок времени выполнения. Например, представим, что мы можем поступить так, как показано в предыдущем примере. Тогда мы бы могли нарушить работу нашего списка (С#):
List<string> ls = new List<string>();
ls.Add("Hello");
List<object> lo = ls; // Представим, что мы можем так сделать
lo[0] = new object(); // Ошибка! lo[0] - ссылка на строку,
// нельзя привести object к string.
Далее мы немного отойдём от вариантности типов обобщений, однако чуть позже вернёмся к этой теме.
Ковариация массивов
С другой стороны то, что мы пытались проделать с типами обобщений разрешено для массивов:
object[] a = string[10]; // Всё ок!
Массивы в С# (как и в Java) — ковариантные. И при неправильной работе с ними во время выполнения мы можем получить неожиданное исключение приведения типов. Так, ковариантность в нашем случае позволяет расширить тип аргумента массива до более широкого (базового).
Дело в том, что ковариация типо-безопасна применительно к операции чтения. Пока мы читаем значения из нашего массива — всё в порядке, однако стоит нам записать в приведённый выше массив данные несоответствующего типа, как мы можем получить ошибку.
Ковариация типов переопределяемых методов в Java
Java (и С++) поддерживают ковариация для типов возвращаемых значений виртуальных методов (Java):
class Animal {
public Animal getAnimal() {
return null;
}
}
class Cat extends Animal {
@Override public Cat getAnimal() {
return null;
}
}
Ковариация и контрвариация делегатов в С#
Чуть ли не единственное место (кроме массивов), где в С# разрешена ковариация — это делегаты. В этом случае ковариантность позволяет методу, возвращающему делегат иметь тип, дочерний по отношению к тому, что определён в делегате (C#):
class A { }
class B : A { }
class Program
{
public delegate A HandlerMethod();
public static A FirstHandler() { return null; }
public static B SecondHandler() { return null; }
static void Main()
{
HandlerMethod handler1 = FirstHandler;
// Ковариация
HandlerMethod handler2 = SecondHandler;
}
}
Далее же мы увидим иллюстрацию ещё одного понятия — контрвариации. Она так же доступна при работе с делегатами. Контрвариация противоположна ковариации. Тогда как ковариация используется для чтения и безопасна в этом отношении, контрвариация используется для записи. Так, контрвариация в нашем случае позволяет методу иметь параметры с типами, которые являются родительскими для тех, что определены в делегате. Так (C#):
class A { }
class B : A { }
class C : B { }
class Program
{
public delegate void hC(C a);
public delegate void hB(B b);
public static void MultiHandler(A a)
{
Console.WriteLine(a.GetType());
}
static void Main(string[] args)
{
hC hc = MultiHandler;
hB hb = MultiHandler;
hc(new C());
hb(new B());
}
}
Подробнее о вариантности типов делегатов можно почитать в MSDN. А мы вернёмся к обобщениям.
Ковариация и обобщения Java
В было сказано, в Java, как и в С#, типы обобщений инвариантны. Однако в Java мы можем использовать подстановочные типы, чтобы добиться необходимой вариантности.
Вот как можно получить ковариантность в нашем примере со списком (Java):
ArrayList<? extends Object> l = new ArrayList<String>();
Так, мы имеем ковариантность, мы можем считывать значения, поскольку они точно приводимы к Object, однако запись значений нам запрещена по причинам, указанным в первой главе. Мы немного изменим наш пример и поэксперементируем (Java):
class Animal {}
class Cat extends Animal {}
// ...
ArrayList<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
// OK - ковариантность
ArrayList<? extends Animal> animals = cats;
Animal a = animals.get(0);
// Не скомпилируется. Ошибка -- запись не типобезопасна
a.add(new Animal());
Контрвариация и обобщения Java
Контрвариация в обобщениях Java реализуется путём указания ограничений супертипов. Контрвариация типобезопасна к записи. Компилятор не знает точного типа для метода add, но ему можно передавать любые объекты Cat и его потомков. Однако при вызове метода get неизвестен конкретный тип возвращаемого значения, поэтому здесь его мы можем присвоить только переменной Object (Java):
ArrayList<Animal> animals = new ArrayList<Animal>(); ArrayList<? super Cat> myanimals = animals; myanimals.add(new Cat()); // Ок - контрвариация // Не известен конкретный тип, так нельзя... Cat cat = myanimals.get(0); // ... поэтому только так: Object o = myanimals.get(0);
Вариантности и обобщения в .NET
Как было сказано, в С# типы обобщений строго инвариантны. Однако это ограничение самого языка, на уровне IL мы можем использовать ковариацию и контрвариацию для интерфейсов. Так, если бы С# это поддерживал, мы могли бы писать так:
class Animal { }
class Cat : Animal { }
interface ICovariantEnumerator<+T> { // + означает ковариацию
T Current { get; }
bool MoveNext();
}
interface ICovariantEnumerable<+T> {
ICovariantEnumerator <T> GetEnumerator();
}
// …
ICovariantEnumerable<Cat> cats = ...
ICovariantEnumerable<Animal> animals = cats; // всё ок
Однако С# это не поддерживает и мы можем воспользоваться одним из «обходных путей», о которых подробно рассказано в MSDN.
Заключение
В статье были рассмотрены вопросы вариации типов. Так, Java поддерживает ковариацию и контрвариацию типов обобщений посредством подстановочных типов (wildcards), С# же такой возможности не предоставляет. Однако оба языка поддерживают ковариацию массивов. Использование вариаций может предоставлять большую мощь при разработке, однако эта тема достаточно нетривиальна. Подробнее о практической составляющей использования вариаций в реальных языках программирования вы можете почитать, пройдя по нижеприведённым ссылкам. Теоретическую же часть можно найти в учебниках по теории категорий.
Ссылки
• Wikipedia – Covariance and contravariance
• MSDN – Variance in Generic Types (C# Programming Guide)
• Rick Byers – Generic type parameter variance in the CLR
• Rick Byers – More on generic variance
• Barry Kelly – Covariance and Contravariance in .NET, Java and C++

Спасибо, статья интересная. Однако мне не понятна польза от практического применения ковариантности обобщений. Если сейчас есть возможность при параметризации дженерика указать некий базовый тип и работать с дженериком (например List) передавая в его методы любые дочерние типы, то разве этого недостаточно?