Для установки нажмите кнопочку Установить расширение. И это всё.

Исходный код расширения WIKI 2 регулярно проверяется специалистами Mozilla Foundation, Google и Apple. Вы также можете это сделать в любой момент.

4,5
Келли Слэйтон
Мои поздравления с отличным проектом... что за великолепная идея!
Александр Григорьевский
Я использую WIKI 2 каждый день
и почти забыл как выглядит оригинальная Википедия.
Статистика
На русском, статей
Улучшено за 24 ч.
Добавлено за 24 ч.
Что мы делаем. Каждая страница проходит через несколько сотен совершенствующих техник. Совершенно та же Википедия. Только лучше.
.
Лео
Ньютон
Яркие
Мягкие

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

Из Википедии — свободной энциклопедии

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

Определения

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

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

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

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

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

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

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

Java

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

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

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

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

C#

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

Обобщённые классы и интерфейсы, появившиеся в 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. В документации Microsoft на русском языке Архивная копия от 24 декабря 2015 на Wayback Machine используются термины ковариация и контравариация.
  2. Здесь и далее слово «производный» не означает «наследник».
  3. Castagna, 1995, Abstract.
  4. On covariance and C++ templates (8 февраля 2013). Дата обращения: 20 июня 2013. Архивировано 28 июня 2013 года.
  5. Eric Lippert. Covariance and Contravariance in C#, Part Two (17 октября 2007). Дата обращения: 22 июня 2013. Архивировано 28 июня 2013 года.

Литература

Эта страница в последний раз была отредактирована 10 октября 2023 в 11:53.
Как только страница обновилась в Википедии она обновляется в Вики 2.
Обычно почти сразу, изредка в течении часа.
Основа этой страницы находится в Википедии. Текст доступен по лицензии CC BY-SA 3.0 Unported License. Нетекстовые медиаданные доступны под собственными лицензиями. Wikipedia® — зарегистрированный товарный знак организации Wikimedia Foundation, Inc. WIKI 2 является независимой компанией и не аффилирована с Фондом Викимедиа (Wikimedia Foundation).