Interested Article - Ковариантность и контравариантность (программирование)
- 2021-05-05
- 1
Ковариа́нтность и контравариа́нтность в программировании — способы переноса наследования типов на производные от них типы — контейнеры , обобщённые типы , делегаты и т. п. Термины произошли от аналогичных понятий теории категорий «ковариантный» и «контравариантный функтор» .
Определения
Ковариантностью
называется сохранение иерархии наследования исходных типов в производных типах в том же порядке. Так, если класс
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>
, и т. п.
См. также
Примечания
- В от 24 декабря 2015 на Wayback Machine используются термины ковариация и контравариация .
- Здесь и далее слово «производный» не означает «наследник».
- , Abstract.
- (8 февраля 2013). Дата обращения: 20 июня 2013. 28 июня 2013 года.
- Eric Lippert. (17 октября 2007). Дата обращения: 22 июня 2013. 28 июня 2013 года.
Литература
- 2021-05-05
- 1