Top-office11.ru

IT и мир ПК
3 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Ковариантность и контравариантность java

Вариантность в программировании

До сих пор не можете спать, пытаясь осмыслить понятия ковариантности и контравариантности? Чувствуете, как они дышат вам в спину, но когда оборачиваетесь ничего не находите? Есть решение!

Меня зовут Никита, и сегодня мы попытаемся заставить механизм в голове работать корректно. Вас ожидает максимально доступное рассмотрение темы вариантности в примерах. Добро пожаловать под кат.

Брифинг

Вариантность в данном посте разбирается безотносительно к какому-либо языку программирования. Примеры в разделе практики написаны на псевдоязыке (он чудом оказался похож на C#) и поэтому не обязаны компилироваться вашим любимым компилятором. Приступим.

Хитрости терминологии

В документации, технической литературе и других источниках вы могли встречаться с различными названиями для явлений вариантности. Больше не стоит пугаться и путаться.

Термины ковариантность и ковариация эквивалентны (по крайней мере в программировании). Более того, термины контравариантность и контравариация также эквивалентны. Так, например, термины ковариантность и контравариантность используется в Википедии и у Троелсена (в переводе). А термины ковариация и контравариация встречаются, например, на MSDN и у Скита (в переводе).

В английском языке всё проще — covariance и contravariance.

Теория

Вариантность — перенос наследования исходных типов на производные от них типы. Под производными типами понимаются контейнеры, делегаты, обобщения, а не типы, связанные отношениями «предок-потомок». Различными видами вариантности являются ковариантность, контравариантность и инвариантность.

Ковариантность — перенос наследования исходных типов на производные от них типы в прямом порядке.
Контравариантность — перенос наследования исходных типов на производные от них типы в обратном порядке.
Инвариантность — ситуация, когда наследование исходных типов не переносится на производные.

Если у производных типов наблюдается ковариантность, говорят, что они ковариантны исходному типу. Если у производных типов наблюдается контравариантность, говорят, что они контравариантны исходному типу. Если у производных типов не наблюдается ни того, ни другого, говорят, что они инвариантны.

Вот и всё, что нужно знать. Конечно, тем кто первый раз сталкивается с вариантностью, трудно вникнуть. Поэтому рассмотрим конкретные примеры.

Практика

Для чего всё это?

Вся суть вариантности состоит в использовании в производных типах преимуществ наследования. Известно, что если два типа связаны отношением «предок-потомок», то объект потомка может храниться в переменной типа предка. На практике это значит, что мы можем использовать для каких-либо операций объекты потомка вместо объектов предка. Тем самым, можно писать более гибкий и короткий код для выполнения действий поддерживаемых разными потомками с общим предком.

Исходная иерархия и производные типы

Для начала опишем иерархию типов, которой будем оперировать. Вверху иерархии у нас находится Device (устройство), потомками которого являются Mouse (мышь), Keyboard (клавиатура). У Mouse в свою очередь тоже есть потомки — WiredMouse (проводная мышь), WirelessMouse (беспроводная мышь).

Все любят контейнеры. На их примере наиболее просто объяснить, что подразумевается под производными типами. Если говорить о списках как производных типах, то для типа Device производным будет
List (список устройств). Аналогично, для типа Keyboard производным будет List (список клавиатур). Думаю, если и были сомнения, то теперь их нет.

Классическая ковариантность

Ковариантность также легче изучать на примере контейнеров. Для этого выделим часть иерархии (ветвь) — Keyboard : Device (клавиатура является устройством, клавиатура частный случай устройства). Опять возьмём списки и построим ковариантную производную ветвь — List : List (список клавиатур является частным случаем списка устройств). Как видим, наследование передалось в прямом порядке.

Рассмотрим пример кода. Есть функция, которая принимает список устройств List и совершает над ними какие-то манипуляции. Как вы уже догадались, в эту функцию можно передать список клавиатур List :

Классическая контравариантность

Каноническим для изучения контравариантности является рассмотрение её на основе делегатов. Допустим, у нас есть обобщённый делегат:

Для исходного типа Device производным будет Action , а для Keyboard — Action . Полученные делегаты могут представлять функции, которые выполняют какие-то действия над устройством или мышью соответственно. Для ветви Keyboard : Device построим производную контравариантную ветвь — Action : Action (действие над устройством является частным случаем действия над клавиатурой — звучит странно, но так и есть). Если можно нажать клавишу на клавиатуре, то это не значит, что и на устройстве можно нажать её (оно может не иметь понятия о том, что такое клавиша). Но если можно подключить устройство, то можно этим же способом (методом, функцией) подключить и клавиатуру. Как видим, наследование передалось в обратном порядке.

Из выше сказанного логично, что если функция может выполнить, что-то над устройством, то она может выполнить это и над клавиатурой. Это значит, мы можем передать объект делегата Action в функцию, принимающую объект делегата Action . Рассмотрим в коде:

Немного инвариантности

Если производные типы инвариантны к исходным типам, то для ветви Keyboard : Device не образуется ни ковариантной ( List : List ), ни контравариантной ( Action : Action ) ветви. Это значит, что нет никакой связи между производными типами. Как видим, наследование не переносится.

А что если?

Неочевидная ковариантность

Делегаты типа Action могут быть ковариантны. Это значит, что для ветви Keyboard : Device образуется ковариантная ветвь — Action : Action . Таким образом, в функцию, принимающую объект делегата Action , можно передавать объект делегата Action .

Неочевидная контравариантность

Контейнеры могут быть контравариантны. Это значит, что для ветви Keyboard : Device образуется контравариантная ветвь — List : List . Таким образом, в функцию, принимающую List , можно передавать List :

Сакральный смысл

Рассмотренные выше экзотические виды вариантности имеют, разве что, академическую ценность. Сложно придумать реальную задачу, которая легче решается при наличии такого рода возможностей. Стоит запомнить, что ковариантность и контравариантность могут вызывать ошибки времени выполнения. Для их устранения требуется вводить определённые ограничения. Компиляторы, как правило, такие ограничения не вводят.

Безопасность для контейнеров

Если производный тип ковариантен, то для обеспечения безопасности контейнер должен быть read only. В противном случае, остаётся возможность записать в List объект неверного типа ( Device , Mouse и другие) через приведение к List :

Если производный тип контравариантен, то для обеспечения безопасности контейнер должен быть write only. В противном случае, остаётся возможность считывания из List объекта неверного типа ( Keyboard , Mouse и других) через приведение к соответствующему списку ( List , List и другим):

Читать еще:  Javascript error emoji is not defined
Двойные стандарты для делегатов

Разумным для делегатов является ковариантность для выходного значения и контравариантность для входных параметров (исключая передачу по ссылке). В случае соблюдения данных условий ошибок времени выполнения не возникает.

Дебрифинг

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

Возможно более правильным определением вариантности является предложенное Эриком Липпертом. Спасибо Alex_sik за ссылку на статью.

Совместимость присваивания, assignment compatibility — это возможность присвоить значение более частного типа совместимой переменной более общего типа.
Вариантность — это сохранение совместимости присваивания исходных типов у производных типов.
Ковариантность — это сохранение совместимости присваивания исходных типов у производных в прямом порядке.
Контравариантность — это сохранение совместимости присваивания исходных типов у производных в обратном порядке.

Ковариантность и контравариантность

Содержание

Введение

Понятие инвариантна появилось в программировании сравнительно давно, благодаря использованию аксиоматической семантики в языках. Инвариант используется в теории верификации программ для доказательства правильности выпоплнения цикла либо для обозначения непротеворечивого состояния объекта в ОПП парадигме. С постепенным развитием компьютерной науки (Computer Science) она все больше начинает обогащаться терминами и определениями из смежных областей (математика, логика и др.). Ковариантность и контравариантность — можно считать одними из таких понятий, которые вошли в обиход сравнительно недавно и наверно наиболее популярными стали с появлением язка Scala где их реализация представленна особенно широко. В данной заметке в кратком виде приводятся определения данных терминов с точки зрения математики, теории категорий и программирования.

Математика

Определим три отображения для целых чисел:

  • D : x → x + x (удвоение)
  • N : x → 0 — x (отрицание)
  • S : x → x * x (возведение в квадрат)

Рассмотрим отношение порядка в двух различных пространствах:

Будет ли оно выполняться при переходе от одного пространства к другому?

Удвоение

  • (x ≤ y) = (D(x) ≤ D(y)) — выполняется

Пример: 2 ≤ 3 = (D(2) ≤ D(3)) = 4 ≤ 6
Отношение порядка выполняется — вариантность

Отрицание:

  • (x ≤ y) = (N(x) ≤ N(y)) — не выполняется
  • (x ≤ y) = (N(y) ≤ N(x)) — выполняется

Пример: 2 ≤ 3 ≠ (N(2) ≤ N(3)) = -2 ≤ -3, однако 2 ≤ 3 = (N(3) ≤ N(2)) = -3 ≤ -2
Отношение порядка меняется на потивоположное — контравариантность

Возведение в квадрат:

  • (x ≤ y) = (S(x) ≤ S(y)) — не выполняется
  • (x ≤ y) = (S(y) ≤ S(x)) — не выполняется

Пример: -3 ≤ -2 ≠ (S(-3) ≤ S(-2)) = 6 ≤ 4, и 2 ≤ 3 ≠ (S(3) ≤ S(2)) = 6 ≤ 4
Отношение порядка не выполняется в обоих случаях- инвариантность

В качестве еще одного примера рассмотрим функцию f: x → sin(x)
Если систему координат сдвинуть на +a по оси X, то график функции сместится в противоположном направлении на -a т.е. при смене базиса (системы координат) компоненты изменяются с помощью преобразования обратного преобразованию базиса — функция является контравариантной относительно оси X. В то же время если сдвинуть систему координат по оси Y а +a, то график функции сместится в том же направленннии — функция является ковариантной относительно оси Y.

Теория категорий

Взаимосвязь между теорией категорий и теорией типов можно выразить следующим образом [1]:

  • Объект — Тип данных
  • Морфизм — Программа

В качестве стандартного примера рассмотрим функтор степени множеств: P: Set → Set [7].

Ковариантный функтор степени P: Set → Set ставит в соответсвие каждому множеству A его множество-степень P(A), а каждой функии f: A → B — отоброжение P(f): P(A) → P(B), переводящий любое подмножетсво X ⊆ A в его образ f(X) ⊆ B.

Если f: a → a * a, тогда множество A = [1, 2] посредством f: A → B будет отображено в B = [1, 4], при этом PA = [[], [1], [2], [1, 2]] в случае если Pf = f: a → a * a будет отображено в PB = [[], [1], [4], [1, 4]]

Контравариантный функтор степени P: Set → Set ставит в соответсвие каждому множеству A его множество-степень P(A), а каждой функии f: A → B — отоброжение P(f): P(B) → P(A), переводящий любое подмножетсво X ⊆ B в его образ f -1 (X) ⊆ A.

Если f: a → a * a, тогда множество A = [1, 2] посредством f: A → B будет отображено в B = [1, 4], при этом PB = [[], [1], [4], [1, 4]] в случае если Pf = f -1 : a → a * a будет отображено в PA = [[], [1], [2], [1, 2]]

Программирование

Для возможности рассуждать об отношении порядка между типами (тип/подтип) необходимо какое-то формальное правило. Такое правило было предложено Барбарой Лисков в 1987 году на конференции в основном докладе под названием «Абстракция данных и иерархия» [7].

Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Другими словами, если некоторый тип S можно подставить везде, где используется тип Т и поведение программы не будет меняться, то тип T является базовым по отношению к S при этом подразумевается, что тип S поддерживает все те же операции что и тип T и при этом все операции типа S требуют меньшего, а предоставляют большее чем соответсвующие операции в T.

Исходя из этого определения можно дать понятия ко-/контр-вариантных типов

  • Ковариантность — случай когда более конкретный тип S может быть подставлен вместо более обобщенного типа Т
  • Контрвариантность — случай когда более общий тип Т может быть подставлен вместо более конкретного типа S
  • Инвариантность — случай когда подставлять можно только определенный тип

При ковариантности иерархия наследования сохраняется в прямом направлении, при контравариантности она меняется на противоположное, а при инвариантности не определена.

Обычно ковариантность и контравариантность смешиваются в одном типе, например для типа произвольной функции которая является контравариантной по своим входным аргументам и ковариантной по своим выходным аргументам т.к. аргументы это нечто что требуется в то время как результат это нечто что предоставляется.

Читать еще:  Приложения на javascript

Еще одним важным следствием из вышеприведенных определений является то, что ковариантность типо-безопасна для операций чтения, а контравариантность для операций записи.

Чтобы все это уяснить, лучше обратиться к реальным примерам.

Возьмем следующую иерархию типов:

Т.к. массивы в Java решено было сделать ковариантными, то вместе с отношением тип B является подтипом A, вводится отношение тип Array является подтипом Array . Такой подход был обусловлен желанием разработчиков Java предоставить возможность реализовывать обобщенные функции (когда обобщенных типов еще не было в языке) в которые можно передавать произвольный ссылочный тип.

Однако это приводит к тому, что допускается не типо-безопасное присваивание и соответсвенно некорректное поведение на runtime (вспомните, что ковариантность типо-безопасна для операций чтения, а не записи):

С появлением обощенных типов (generics), которые по умолчанию инвариантны необходимость в таком подходе отпала, но для обратной совместимости поведение было сохранено.

Иногда, все-же, для увеличения гибкости есть необходимоть сделать коллекции ковариантными/контравариантными. Java предоставляет такую возможность посредством специального вида типа с параметрами, называемого ограниченным типом групповых символов (wildcard-тип).

Ковариантность

Для объявления коллекции ковариантной можно использовать констркуцию ? extends, однако в этом случае будут доступны только операции чтения, а при попытке вызова не типо-безопасных операций записи возникнет ошибка при компиляции. Например нельзя добавить елементы типа B т.к. коллекция может ссылаться на любой из подтипов типа А.

В результате можно реализовать какую-то обобщенную функцию не изменющую исходную коллекию (например вывод на экран всех елементов) для типа А, и использовать ее для елементов являющимися подтипами А (B, C или D).

Контравариантность

Для объявления коллекции контравариантной можно использовать констркуцию ? super, однако теперь будут доступны только операции записи, а при попытке вызова операций чтения возникнет ошибка при компиляции.

Механизм объявления ко/контравариатных отношений в Java иногда еще назвают PECS (producer-extends, consumer-super). Тип предоставляющий данные (которые будут читаться) объявляется при помощи , а тип предназначенный для потребителя (который необходимо наполнить) объявляется при помощи

В качестве реального примера можно рассмотреть упрощенную реализацию стека.

Ковариантность/контравариантность также свойственна для иерархии наследования.

Как видно из примеров ковариантность свойственна выходным параметрам, а контравариантность — входным.

В C# нет аналога wildcard типов из Java, однако с версии Visual C# 2010 ковариантность/контравариантность была добавлена в обобщенных интерфейсах и типах делегатов. Интерфейс IEnumerable является ковариантным, а например IComparer контравариантным. [11]

Пример того как можно определить ковариантный/контравариантный интерфейсы:

Пример реализации стека на C#, аналогичный приведенному выше для Java:

Из-за того что в C# ковариантными являются только некоторые интерфейсы и делегаты полностью воспроизвести поведение для метода popAll как в Java не получится, но можно реализовать ForEach который достанет все елементы и передаст их в делегат action. Использоваться может так:

В C++ нет обобщенных типов и как следствие параметрического полиморфизма (шаблонные типы представляют собой просто кодогенерацию). Основной вид полиморфизма — полиморфизм наследования.

Ковариантность

Контравариантность

Scala

Scala обладает гораздо более широкой системой типов по сравнению с Java или C#. Для возможности определения отношений между типами в язык введены аннотации вариативности (variance annottions), которые задаются при помощи префиксов перед типовыми переменными: «+» в случае ковариантного типа либо «-» для контравариантного.

Для проверки корректности аннотаций вариативности, компилятор классифицирует все позиции в теле класса или примеси на положительные, отрицательные или нейтральные. Типовой параметр аннотированный «+» может быть использован только в положительной позиции тогда как аннотированный при помощи «-» только в отрицательной. Типовые параметры без аннотации могут использоваться в любой позиции [12]

Определение функции с одним аргументом выглядит следующим образом:

Компилятор строго отслеживает в какой позиции находятся параметры и не допускает их неправильного использования. Тем не менее для большей гибкости иногда возникает необходимость обойти эти ограничения. Для этого в язык введено понятие ограничивающего полиморфизма — F-bounded polymorphism [13]. Определение класса для реализации стека (аналогичному приведенному выше) с использованием различных вариантов может выглядеть следующим образом:

Haskell

В Haskell нет изменяемых значений, поэтому все типы ковариантны. В то же время в языке активно используется понятие функтора, который, как было показано выше (см. Теория категорий), может быть как ковариантным так и контравариантным. Само поянтие, как и монаидальные типы для работы с которыми он предназначен, было позаимствовано из теории категорий.

Пример ковариантного фуктора, имеющегося в стандартной библиотеке:

Определение контравариантного функтора тривиальна:

В качестве примера можно рассмотреть вариант использования contrmap принимающую функцию, которая вычисляет длину списка, для вычисления длины множества.

Определения контравариантного функтора в стандартной библиотеке нет, но на http://hackage.haskell.org можно найти готовый пакет [15].

Дополнение

Используемые версии компиляторов:

java version «1.8.0_05»
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

GCC

g++ (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)

Mono

Mono C# compiler version 3.2.8.0

Scala

Scala code runner version 2.11.1 — Copyright 2002-2013, LAMP/EPFL

Haskell

The Glorious Glasgow Haskell Compilation System, version 7.6.3

Инвариантность, ковариация и контравариантность в Java

Java урок по дженерикам приводит меня к variance. Это вызывает у меня головную боль, поскольку я не могу найти очень простой демонстрации того, что это такое.

Я прочитал несколько похожих вопросов в stackoverflow, но я счел их слишком трудными для понимания Java-учеником. На самом деле проблема заключается в том, что объяснение дженериков требует понимания дисперсии, и концепция дисперсии демонстрируется в значительной степени в отношении понимания дженериков.

У меня была надежда прочитать этот, но в конце я поделился C. R. чувство:

Название напоминает мне о днях, изучающих общую теорию относительности. — C.R. Дек 22 ’13 в 7:34

Четыре вопроса теории очень сбивают меня с толку, и я не могу найти хороших и простых объяснений. Вот они, с моим текущим неполным пониманием (я боюсь, что экспертам будет очень интересно читать это).

Читать еще:  Java sql oracle

Ваша помощь в исправлении и уточнении приветствуется (помните, это для новичков, а не для экспертов).

Что-то не так с этим пониманием?

  • Что такое инвариантность/ковариация/контравариантность, связанная в контексте программирования? Мое лучшее предположение:
    • Это что-то встречается в объектно-ориентированном программировании.
    • Это нужно делать при просмотре аргументов метода и типа результата в классе и предке.
    • Это используется в контексте метода overriding и overloading.
    • Это используется для установления соединения между типом аргумента метода или возвращаемым типом метода и наследованием самих классов, например. если класс D является потомком класса A, что мы можем сказать о типах аргументов и методе возврата метода метода?
  • Как отклонение относится к методам Java? Мое лучшее предположение состоит в том, что, учитывая два класса A и D, с A, являющимся предком D, и методом overhiden/overloaded f (arg):
    • Если отношение между типом аргумента в двух методах является ТОЛЬКО, чем отношение между двумя классами, тип аргумента в методе называется COVARIANT с типом класса, иначе говоря: наследование между типами arg в и D ковариантно с наследованием классов A и D.
    • Если отношение между аргументами REVERSES связано между классами, тип arg указан CONTRAVARIANT к типу класса, иначе: наследование между типами arg в и D контравариантно с наследованием классов A и D..
  • Почему такое понимание важности для Java-программистов важно? Я предполагаю, что:
    • Создатели языка Java внедрили правила для дисперсии на языке, и это имеет значение для программирования программиста.
    • В правиле указано, что возвращаемый тип метода переопределения/перегрузки должен быть контравариантным для наследования.
    • В другом правиле указано, что тип аргумента переопределения/перегрузки должен быть ковариантным для наследования.
    • Компилятор Java проверяет правильность правил отклонения и соответственно предоставляет ошибки или предупреждения. Расшифровка сообщений проще с помощью знаний о различиях.
  • В чем разница между переопределением и перегрузкой? Лучшее предположение:
    • Метод переопределяет другой метод, когда аргументы и возвращаемые типы являются инвариантными. Все остальные случаи понимаются компилятором как перегрузка.

Это не относится к OO, но имеет отношение к свойствам определенных типов.

Например, с типом функции

Пусть C — подтип A, а D — подтип B. Тогда справедливо следующее:

но следующие недопустимы:

следовательно, чтобы проверить, действительно ли вызов метода meth, мы должны проверить

  • Ожидаемый тип возвращаемого значения — это супертип объявленного типа возврата.
  • Фактический тип аргумента — это подтип объявленного типа аргумента.

Это все хорошо известно и интуитивно понятно. По соглашению мы говорим, что возвращаемый тип функции ковариантен, а тип аргумента метода контравариантен.

С параметризованными типами, такими как List, мы имеем его, что тип аргумента инвариантен в таких языках, как Java, где мы имеем изменчивость. Мы не можем сказать, что список C — это список A, потому что, если бы это было так, мы могли бы хранить A в списке Cs, что бы удивить вызывающего, который принимает только Cs в списке. Однако в языках, где значения неизменяемы, например, Haskell, это не проблема. Поскольку данные, которые мы передаем в функции, не могут быть мутированы, список C фактически является списком A, если C является подтипом A. (Обратите внимание, что Haskell не имеет реального подтипирования, но вместо этого имеет отношение к понятию «более/менее полиморфный» типы.)

Ковариантность и контрвариантность

28.07.2016, 23:25

Ковариантность и контрвариантность
Добрый день, в книге дошел до ковариантности и контравариантности и появился вопрос. Если с.

Ковариантность (и контрвариантность) массивов
Найдите и исправьте ошибки в методе Main using System; namespace Less05_task01 < class.

Ковариантность и Контрвариантность — что это?
Прошу помочь мне объяснить эти два понятия, ещё читая Шилдта не понял, что эта за возможность.

ковариантность
ковариантность позволяет использовать override метод, чья ссылка на сам объект принадлежит базовому.

29.07.2016, 00:202

Там, где в качестве типов у обобщенных интерфейсов присутствуют наследуемые друг от друга классы.

31.07.2016, 15:08 [ТС]331.07.2016, 15:36431.07.2016, 19:095

Нет, это обычный полиморфизм.

В дискретной математике (которую, судя по нику, вы должны изучать) есть такое понятие, как отношение, применяемое к множествам и их элементам.
Например, на множестве целых чисел ℤ между двумя элементами a и b можно определить бинарное отношение эквивалентности — это когда элемент a равен элементу b.
Более интересно в контексте рассматриваемой темы так называемое отношение порядка — это когда, например a ≤ b или b ≤ a.
Если имеется некая функция X (которая тоже в свою очередь является отношением отображения, или просто отображением или проекцией), то эта функция называется ковариантной, если ее результат сохраняет отношение порядка элементов, к которым она применяется, то есть если a ≤ b, то функция Х является ковариантной, если X(a) ≤ X(b).
Контравариантной функция Х называется тогда, когда результаты функций в отношении порядка меняются местами, то есть если a ≤ b, то функция Х является контравариантной, если X(b) ≤ X(a).

Перенося это на систему типов в шарпе, множество можно определить как множество всех типов, а отношение порядка заменить отношением «ссылку на объект типа b можно присвоить переменной типа a» или более серьезным языком: на отношение совместимости типов по присваиванию.
Если определить отношение a

Если применить к этой проекции отношение совместимости по присваиванию, то интерфейс будет ковариантным, если направление отношения сохраняется:

  • I // Переменной типа I можно неявно присвоить ссылку на объект типа I
  • I // Переменной типа I можно неявно присвоить ссылку на объект типа I
  • I // Переменной типа I можно неявно присвоить ссылку на объект типа I

Если поменять местами элементы отношения, то интерфейс станет контравариантным:

  • I // Переменной типа I можно неявно присвоить ссылку на объект типа I
  • I // Переменной типа I можно неявно присвоить ссылку на объект типа I
  • I // Переменной типа I можно неявно присвоить ссылку на объект типа I

Как видите, отличие от обычного полиморфизма в том, что отношение совместимости по присваиванию применяется не напрямую к типам, а к некой проекции над этими типами. В данном примере — к обобщенным интерфейсам.

Если переписывать ваш пример под вариантность, то выглядеть он будет так:

Ссылка на основную публикацию
Adblock
detector