У меня проблемы с пониманием разницы между ковариацией и контравариантностью.
источник
У меня проблемы с пониманием разницы между ковариацией и контравариантностью.
Вопрос в том, в чем разница между ковариацией и контравариантностью?
Ковариантность и контравариантность являются свойствами отображающей функции, которая связывает один элемент набора с другим . Более конкретно, отображение может быть ковариантным или контравариантным относительно отношения на этом множестве.
Рассмотрим следующие два подмножества множества всех типов C #. Первый:
{ Animal,
Tiger,
Fruit,
Banana }.
И во-вторых, это явно связанный набор:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
Существует операция отображения из первого набора во второй набор. То есть для каждого T в первом наборе соответствующий тип во втором наборе равен IEnumerable<T>
. Или, в кратком изложении, отображение T → IE<T>
. Обратите внимание, что это «тонкая стрела».
Со мной так далеко?
Теперь давайте рассмотрим отношение . Существует отношение совместимости присваивания между парами типов в первом сете. Значение типа Tiger
может быть присвоено переменной типа Animal
, поэтому эти типы называются «совместимыми по присваиванию». Записи Давайте «значение типа X
может быть присвоено переменной типа Y
» в краткой форме: X ⇒ Y
. Обратите внимание, что это «жирная стрела».
Итак, в нашем первом подмножестве приведены все отношения совместимости назначений:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
В C # 4, который поддерживает ковариантную совместимость присваивания определенных интерфейсов, существует отношение совместимости присваивания между парами типов во втором наборе:
IE<Tiger> ⇒ IE<Tiger>
IE<Tiger> ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit> ⇒ IE<Fruit>
Обратите внимание, что отображение T → IE<T>
сохраняет существование и направление совместимости назначения . То есть, если X ⇒ Y
, то это тоже правда IE<X> ⇒ IE<Y>
.
Если у нас есть две вещи по обе стороны от жирной стрелки, то мы можем заменить обе стороны чем-то с правой стороны соответствующей тонкой стрелки.
Отображение, которое обладает этим свойством относительно определенного отношения, называется «ковариантным отображением». Это должно иметь смысл: последовательность Тигров может использоваться там, где требуется последовательность Животных, но обратное неверно. Последовательность животных не обязательно может быть использована там, где необходима последовательность тигров.
Это ковариация. Теперь рассмотрим это подмножество множества всех типов:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
Теперь у нас есть отображение из первого набора в третий набор T → IC<T>
.
В C # 4:
IC<Tiger> ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger> Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit> ⇒ IC<Banana> Backwards!
IC<Fruit> ⇒ IC<Fruit>
То есть, отображение T → IC<T>
уже сохраняется существование , но обратное направление совместимости назначения. То есть если X ⇒ Y
тогда IC<X> ⇐ IC<Y>
.
Отображение, которое сохраняет, но обращает отношение, называется контравариантным отображением.
Опять же, это должно быть четко правильно. Устройство, которое может сравнивать двух животных, также может сравнивать двух тигров, но устройство, которое может сравнивать двух тигров, не обязательно может сравнивать любые два животных.
Так что в C # 4 разница между ковариацией и контравариантностью. Ковариация сохраняет направление присваиваемости. Контравариантность полностью изменяет это.
IEnumerable<Tiger>
вIEnumerable<Animal>
безопасно? Потому что нет никакого способа ввести жирафа вIEnumerable<Animal>
. Почему мы можем преобразоватьIComparable<Animal>
вIComparable<Tiger>
? Потому что нет никакого способа вынуть жирафа изIComparable<Animal>
. Есть смысл?Привести примеры, наверное, проще всего - я их точно помню.
ковариации
Канонические примеры:
IEnumerable<out T>
,Func<out T>
Вы можете конвертировать
IEnumerable<string>
вIEnumerable<object>
илиFunc<string>
вFunc<object>
. Значения только выходят из этих объектов.Это работает, потому что, если вы берете значения только из API и собираетесь возвращать что-то конкретное (например
string
), вы можете рассматривать это возвращаемое значение как более общий тип (напримерobject
).контрвариация
Канонические примеры:
IComparer<in T>
,Action<in T>
Вы можете конвертировать
IComparer<object>
вIComparer<string>
, илиAction<object>
кAction<string>
; значения входят только в эти объекты.На этот раз это работает, потому что если API ожидает чего-то общего (например
object
), вы можете дать ему что-то более конкретное (напримерstring
).В более общем смысле
Если у вас есть интерфейс,
IFoo<T>
он может быть ковариантнымT
(т.е. объявлять его так, какIFoo<out T>
будтоT
он используется только в выходной позиции (например, тип возврата) внутри интерфейса. Он может быть контравариантен вT
(то естьIFoo<in T>
), еслиT
используется только во входной позиции ( например, тип параметра).Это может привести к путанице, потому что «выходная позиция» не так проста, как кажется - параметр типа
Action<T>
все еще используется толькоT
в выходной позиции - противоположностьAction<T>
поворота, если вы понимаете, что я имею в виду. Это «выход» в том смысле, что значения могут передаваться от реализации метода к коду вызывающей стороны, так же как и возвращаемое значение. Обычно такого рода вещи не подходят, к счастью :)источник
Action<T>
все еще используется толькоT
в выходной позиции» .Action<T>
возвращаемый тип void, как его использоватьT
в качестве вывода? Или это то, что он означает, потому что он не возвращает ничего, что вы можете видеть, что оно никогда не может нарушить правило?Я надеюсь, что мой пост поможет получить не зависящее от языка представление темы.
Для наших внутренних тренингов я работал с замечательной книгой «Smalltalk, Objects and Design (Chamond Liu)» и перефразировал следующие примеры.
Что означает «согласованность»? Идея состоит в том, чтобы спроектировать безопасные для типов иерархии типов с сильно замещаемыми типами. Ключом к получению этой согласованности является соответствие на основе подтипов, если вы работаете на языке со статической типизацией. (Мы обсудим принцип замены Лискова (LSP) здесь на высоком уровне.)
Практические примеры (псевдокод / неверный в C #):
Ковариантность: Давайте предположим, что птицы, которые откладывают яйца «последовательно» со статической типизацией: если тип Bird откладывает яйцо, не подтип Bird подкладывает подтип яйца? Например, тип Duck откладывает DuckEgg, затем задается последовательность. Почему это соответствует? Потому что в таком выражении:
Egg anEgg = aBird.Lay();
ссылка aBird может быть юридически заменена Bird или экземпляром Duck. Мы говорим, что возвращаемый тип ковариантен типу, в котором определен Lay (). Переопределение подтипа может возвращать более специализированный тип. => «Они доставляют больше.»Contravariance: Давайте предположим, что пианино, что пианисты могут играть «последовательно» со статической типизацией: если пианист играет на пианино, сможет ли она играть на рояле? Разве виртуоз не хотел бы играть на рояле? (Будьте осторожны, есть поворот!) Это противоречиво! Потому что в таком выражении:
aPiano.Play(aPianist);
aPiano не может быть юридически заменено Piano или экземпляром GrandPiano! Играть на GrandPiano может только виртуоз, пианисты слишком общие! GrandPianos должны воспроизводиться более общими типами, тогда игра будет последовательной. Мы говорим, что тип параметра противоположен типу, в котором определяется Play (). Переопределение подтипа может принимать более обобщенный тип. => «Они требуют меньше.»Назад к C #:
поскольку C # в основном является языком статической типизации, «местоположения» интерфейса типа, которые должны быть ко-или контравариантными (например, параметры и возвращаемые типы), должны быть помечены явно, чтобы гарантировать последовательное использование / развитие этого типа , чтобы LSP работал нормально. В динамически типизированных языках согласованность LSP обычно не является проблемой, другими словами, вы могли бы полностью избавиться от ко-и контравариантной «разметки» на интерфейсах и делегатах .Net, если бы вы использовали только тип dynamic в ваших типах. - Но это не лучшее решение в C # (вы не должны использовать динамические в публичных интерфейсах).
Вернуться к теории:
Описанное соответствие (ковариантные возвращаемые типы / контравариантные типы параметров) является теоретическим идеалом (поддерживаемым языками Emerald и POOL-1). Некоторые oop языки (например, Eiffel) решили применить другой тип согласованности, особенно. также ковариантные типы параметров, потому что он лучше описывает реальность, чем теоретический идеал. В статически типизированных языках желаемая согласованность часто должна достигаться путем применения шаблонов проектирования, таких как «двойная диспетчеризация» и «посетитель». Другие языки предоставляют так называемые «множественные диспетчеризации» или множественные методы (это, в основном, выбор перегрузки функций во время выполнения , например, с CLOS) или получают желаемый эффект с помощью динамической типизации.
источник
Bird
определяетpublic abstract BirdEgg Lay();
, тоDuck : Bird
ДОЛЖЕН реализовывать.public override BirdEgg Lay(){}
Таким образом, ваше утверждение, котороеBirdEgg anEgg = aBird.Lay();
имеет какую-либо дисперсию, просто не соответствует действительности. Будучи предпосылкой точки объяснения, весь смысл теперь ушел. Вместо этого вы бы сказали, что ковариация существует внутри реализации, где DuckEgg неявно приводится к типу BirdEgg out / return? В любом случае, пожалуйста, устраните мою путаницу.DuckEgg Lay()
не является корректным переопределением дляEgg Lay()
в C # , и это суть. C # не поддерживает ковариантные возвращаемые типы, но Java, как и C ++, поддерживают. Я скорее описал теоретический идеал, используя C # -подобный синтаксис. В C # вам нужно позволить Bird и Duck реализовать общий интерфейс, в котором определено, что Lay имеет ковариантный тип возврата (т. Е. Вне спецификации), и тогда все сходится!extends
, Consumersuper
».Делегат конвертера помогает мне понять разницу.
TOutput
представляет ковариацию, где метод возвращает более конкретный тип .TInput
представляет собой противоположность, где метод передается менее конкретный тип .источник
Дисперсия Co и Contra - довольно логичные вещи. Система языковых типов заставляет нас поддерживать логику реальной жизни. Это легко понять на примере.
ковариации
Например, вы хотите купить цветок, и у вас есть два магазина цветов в вашем городе: магазин роз и магазин ромашек.
Если вы спросите кого-то "где магазин цветов?" а кто-то скажет вам, где магазин роз, все будет в порядке? Да, потому что роза - это цветок, если вы хотите купить цветок, вы можете купить розу. То же самое относится, если кто-то ответил вам адресом магазина ромашки.
Это пример ковариации : вам разрешено гипс
A<C>
кA<B>
, гдеC
это подклассB
, еслиA
производят общие значения (возвращают в качестве результата от функции). Ковариантность о производителях, поэтому C # использует ключевое словоout
для ковариации.Типы:
Вопрос «где магазин цветов?», Ответ «магазин роз»:
контрвариация
Например, вы хотите подарить цветок своей девушке, а ваша девушка любит любые цветы. Можете ли вы считать ее человеком, который любит розы или человеком, который любит ромашки? Да, потому что, если она любит любой цветок, она будет любить и розу и маргаритку.
Это пример контрвариации : вы позволили гипс
A<B>
кA<C>
, гдеC
есть подклассB
, еслиA
истребляет общее значение. Contravariance касается потребителей, поэтому C # использует ключевое словоin
для контравариантности.Типы:
Вы рассматриваете свою девушку, которая любит любой цветок, как человека, который любит розы, и дарите ей розу:
связи
источник