Interested Article - Ковариантность и контравариантность (программирование)

Ковариа́нтность и контравариа́нтность в программировании — способы переноса наследования типов на производные от них типы — контейнеры , обобщённые типы , делегаты и т. п. Термины произошли от аналогичных понятий теории категорий «ковариантный» и «контравариантный функтор» .

Определения

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

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

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

Контравариантность позволяет корректно устанавливать тип при создании подтипов (subtyping), то есть, установить множество функций, позволяющее заменить другое множество функций в любом контексте. В свою очередь, ковариантность характеризует специализацию кода , то есть замену старого кода новым в определённых случаях. Таким образом, ковариантность и контравариантность являются независимыми механизмами типобезопасности , не исключающими друг друга, и могут и должны применяться в объектно-ориентированных языках программирования .

Использование

Массивы и другие контейнеры

В контейнерах , допускающих запись объектов, ковариантность считается нежелательной, поскольку она позволяет обходить контроль типов. В самом деле, рассмотрим ковариантные массивы. Пусть классы Cat и Dog наследуют от класса Animal (в частности, переменной типа Animal можно присвоить переменную типа Cat или Dog ). Создадим массив Cat[] . Благодаря контролю типов в этот массив можно записывать лишь объекты типа Cat и его потомков. Затем присвоим ссылку на этот массив переменной типа Animal[] (ковариантность массивов это позволяет). Теперь в этот массив, известный уже как Animal[] , запишем переменную типа Dog . Таким образом, в массив Cat[] мы записали Dog , обойдя контроль типов. Поэтому контейнеры, разрешающие запись, желательно делать инвариантными. Также, контейнеры, разрешающие запись, могут реализовывать два независимых интерфейса, ковариантный Producer<T> и контравариантный Consumer<T>, в этом случае вышеописанный обход контроля типов сделать не удастся.

Поскольку контроль типов может нарушаться лишь при записи элемента в контейнер, то для неизменяемых коллекций и итераторов ковариантность безопасна и даже полезна. Например, с её помощью в языке C# любому методу, принимающему аргумент типа IEnumerable<Object> , можно передавать любую коллекцию любого типа, например IEnumerable<String> или даже List<String> .

Если же в данном контексте контейнер используется, наоборот, только для записи в него, а чтение отсутствует, то он может быть контравариантным. Так, если есть гипотетический тип WriteOnlyList<T> , наследующий от List<T> и запрещающий в нём операции чтения, и функция с параметром WriteOnlyList<Cat> , куда она записывает объекты типа Cat , то передавать ей List<Animal> или List<Object> безопасно — туда она ничего, кроме объектов класса-наследника, не запишет, а пытаться читать другие объекты не будет.

Функциональные типы

В языках с функциями первого класса существуют обобщённые функциональные типы и переменные- делегаты . Для обобщённых функциональных типов полезна ковариантность по возвращаемым типам и контравариантность по аргументам. Так, если делегат задан как «функция, принимающая String и возвращающая Object», то в него можно записать и функцию, принимающую Object и возвращающую String: если функция способна принимать любой объект, она может принимать и строку; а из того, что результатом функции является строка, следует, что функция возвращает объект.

Реализация в языках

C++

C++ начиная со стандарта 1998 года поддерживает ковариантные типы возврата в перекрытых виртуальных функциях :

class X {};

class A
{
public:
    virtual X* f() { return new X; }
};

class Y : public X {};

class B : public A
{
public:
    virtual Y* f() { return new Y; } // ковариантность позволяет задать в перекрытом методе уточнённый тип возврата
};

Указатели в C++ ковариантны: например, указателю на базовый класс можно присвоить указатель на дочерний класс.

Шаблоны C++ , вообще говоря, инвариантны, отношения наследования классов-параметров на шаблоны не переносится. Например, ковариантный контейнер vector<T> позволял бы нарушать контроль типов. Однако при помощи параметризованных конструкторов копирования и операторов присваивания можно создать умный указатель , ковариантный своему параметру-типу .

Java

Ковариантность типов возврата методов реализована в Java начиная с J2SE 5.0 . В параметрах методов ковариантности нет: для перекрытия виртуального метода типы его параметров должны совпадать с определением в родительском классе, иначе вместо перекрытия будет определён новый перегруженный метод с этими параметрами.

Массивы в Java ковариантны с самой первой версии, когда в языке ещё не было обобщенных типов . (Если бы этого не было, то для использования, например, библиотечного метода, принимающего массив объектов Object[] , для работы с массивом строк String[] , требовалось бы его сначала скопировать в новый массив Object[] .) Поскольку, как было сказано выше, при записи элемента в такой массив можно обойти контроль типов, в JVM существует дополнительный контроль во время выполнения, генерирующий исключение при записи некорректного элемента.

Обобщённые типы в Java инвариантны, поскольку вместо создания универсального метода, работающего с Object’ами, можно его параметризовать, превратив в обобщённый метод и сохранив контроль типов.

Вместе с тем в Java можно реализовать своего рода ко- и контравариантность обобщенных типов, используя символ-джокер и уточняющие спецификаторы: List<? extends Animal> будет ковариантен подставляемому типу, а List<? super Animal> — контравариантен.

C#

В языке C# , начиная с первой его версии, массивы ковариантны. Это было сделано для совместимости с языком Java . При попытке записать в массив элемент неверного типа выбрасывается исключение во время выполнения.

Обобщённые классы и интерфейсы, появившиеся в C# 2.0, стали, как и в Java, инвариантными по типу-параметру.

С введением обобщённых делегатов (параметризированных по типам аргументов и возвращаемым типам), язык позволил автоматическое преобразование обычных методов к обобщённым делегатам с ковариантностью по возвращаемым типам и контравариантностью по типам аргументов. Поэтому в C# 2.0 стал возможен код следующего вида:

void ProcessString(String s) { /* ... */}
void ProcessAnyObject(Object o) { /* ... */ }
String GetString() { /* ... */ }
Object GetAnyObject() { /* ... */ }
//...
Action<String> process = ProcessAnyObject;
process(myString); // легальное действие

Func<Object> getter = GetString;
Object obj = getter(); // легальное действие

однако код Action<Object> process = ProcessString; некорректен и даёт ошибку компиляции, иначе этот делегат можно было бы потом вызвать как process(5) , передавая Int32 в ProcessString.

В C# 2.0 и 3.0 этот механизм позволял лишь записывать простые методы в обобщённые делегаты и не мог делать автоматическое преобразование одних обобщённых делегатов в другие. Иначе говоря, код

Func<String> f1 = GetString;
Func<Object> f2 = f1;

в этих версиях языка не компилировался. Таким образом, обобщённые делегаты в C# 2.0 и 3.0 всё ещё были инвариантными.

В C# 4.0 это ограничение было снято, и начиная с этой версии код f2 = f1 в примере выше стал работать.

Кроме того, в 4.0 стало возможным задавать вариантность параметров обобщённых интерфейсов и делегатов явным образом. Для этого используются ключевые слова out и in соответственно. Поскольку в обобщённом типе реальное использование типа-параметра известно лишь его автору, к тому же оно может меняться в процессе разработки, это решение обеспечивает наибольшую гибкость без ущерба для надёжности контроля типов.

Некоторые библиотечные интерфейсы и делегаты были переопределены в C# 4.0 с использованием этих возможностей. Например, интерфейс IEnumerable<T> отныне стал определяться как IEnumerable<out T> , интерфейс IComparable<T> — как IComparable<in T> , делегат Action<T> — как Action<in T> , и т. п.

См. также

Примечания

  1. В от 24 декабря 2015 на Wayback Machine используются термины ковариация и контравариация .
  2. Здесь и далее слово «производный» не означает «наследник».
  3. , Abstract.
  4. (8 февраля 2013). Дата обращения: 20 июня 2013. 28 июня 2013 года.
  5. Eric Lippert. (17 октября 2007). Дата обращения: 22 июня 2013. 28 июня 2013 года.

Литература

  • Castagna, Giuseppe. (англ.) // ACM Trans. Program. Lang. Syst.. — ACM, 1995. — Vol. 17 , no. 3 . — P. 431-447 . — ISSN . — doi : .
Источник —

Same as Ковариантность и контравариантность (программирование)