Разница между ковариацией и контрастностью

Ответы:

266

Вопрос в том, в чем разница между ковариацией и контравариантностью?

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

Рассмотрим следующие два подмножества множества всех типов 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 разница между ковариацией и контравариантностью. Ковариация сохраняет направление присваиваемости. Контравариантность полностью изменяет это.

Эрик Липперт
источник
4
Для таких, как я, было бы лучше добавить примеры, показывающие, что НЕ ковариантно, а что НЕ противоречиво, а что нет, и то, и другое.
Бьян
2
@ Баргитта: Это очень похоже. Разница в том, что C # использует определенную дисперсию сайта, а Java использует дисперсию сайта вызова . Таким образом, все меняется одинаково, но когда разработчик говорит: «Мне нужно, чтобы это был вариант», это другое. Кстати, функция на обоих языках была частично разработана одним и тем же человеком!
Эрик Липперт
2
@AshishNegi: прочитайте стрелку как «можно использовать как». «То, что может сравнивать животных, может использоваться как то, что может сравнивать тигров». Есть смысл сейчас?
Эрик Липперт
1
@AshishNegi: Нет, это не правильно. IEnumerable является ковариантным, поскольку T появляется только в возвращаемых методах IEnumerable. И IComparable является контравариантным, потому что T появляется только как формальные параметры методов IComparable .
Эрик Липперт
2
@AshishNegi: Вы хотите подумать о логических причинах , лежащих в основе этих отношений. Почему мы можем конвертировать IEnumerable<Tiger>в IEnumerable<Animal>безопасно? Потому что нет никакого способа ввести жирафа в IEnumerable<Animal>. Почему мы можем преобразовать IComparable<Animal>в IComparable<Tiger>? Потому что нет никакого способа вынуть жирафа из IComparable<Animal>. Есть смысл?
Эрик Липперт
111

Привести примеры, наверное, проще всего - я их точно помню.

ковариации

Канонические примеры: 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>поворота, если вы понимаете, что я имею в виду. Это «выход» в том смысле, что значения могут передаваться от реализации метода к коду вызывающей стороны, так же как и возвращаемое значение. Обычно такого рода вещи не подходят, к счастью :)

Джон Скит
источник
1
Для таких, как я, было бы лучше добавить примеры, показывающие, что НЕ ковариантно, а что НЕ противоречиво, а что нет, и то, и другое.
Бьян
1
@Jon Skeet Хороший пример, я только не понимаю, «параметр типа Action<T>все еще используется только Tв выходной позиции» . Action<T>возвращаемый тип void, как его использовать Tв качестве вывода? Или это то, что он означает, потому что он не возвращает ничего, что вы можете видеть, что оно никогда не может нарушить правило?
Александр Дерк
2
Для моей будущей личности, которая возвращается к этим прекрасному ответу снова переучиваться разницей, это линия вы хотите: «[ковариация] работает , потому что если вы только принимать значения из API, и он собирается вернуть что - то специфично (например, строка), вы можете рассматривать это возвращаемое значение как более общий тип (например, объект). "
Мэтт Кляйн
Самая запутанная часть всего этого заключается в том, что либо для ковариации, либо для контравариантности, если вы игнорируете направление (внутри или снаружи), вы все равно получаете преобразование от более специфичного к более универсальному! Я имею в виду: «вы можете рассматривать это возвращаемое значение как более общий тип (например, объект)» для ковариации и: «API ожидает чего-то общего (например, объект), вы можете дать ему что-то более конкретное (например, строку)» для контравариантности . Для меня это звучит так же!
XMight
@AlexanderDerck: Не знаю, почему я не ответил вам раньше; Я согласен, что это неясно, и постараюсь уточнить это.
Джон Скит
16

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

Для наших внутренних тренингов я работал с замечательной книгой «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? В любом случае, пожалуйста, устраните мою путаницу.
Суамер
1
Короче говоря: вы правы! Извините за путаницу. DuckEgg Lay()не является корректным переопределением для Egg Lay() в C # , и это суть. C # не поддерживает ковариантные возвращаемые типы, но Java, как и C ++, поддерживают. Я скорее описал теоретический идеал, используя C # -подобный синтаксис. В C # вам нужно позволить Bird и Duck реализовать общий интерфейс, в котором определено, что Lay имеет ковариантный тип возврата (т. Е. Вне спецификации), и тогда все сходится!
Нико
1
В качестве аналога комментария Мэтта-Кляйна к ответу @ Jon-Skeet, «моему будущему я»: лучший вывод для меня здесь - «Они доставляют больше» (специфично) и «Они требуют меньше» (специфично). «Требуй меньше и доставь больше» - превосходная мнемоника! Это похоже на работу, где я надеюсь потребовать менее конкретные инструкции (общие запросы) и в то же время предоставить что-то более конкретное (реальный рабочий продукт). В любом случае порядок подтипов (LSP) не нарушается.
Карфус
@karfus: Спасибо, но, насколько я помню, я перефразировал идею «Требуй меньше и доставь больше» из другого источника. Может быть, это книга Лю, о которой я говорю выше ... или даже разговор .NET Rock. Btw. в Java люди понизили мнемонику до «PECS», что напрямую связано с синтаксическим способом объявления отклонений, PECS для «Producer extends, Consumer super».
Нико
5

Делегат конвертера помогает мне понять разницу.

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputпредставляет ковариацию, где метод возвращает более конкретный тип .

TInputпредставляет собой противоположность, где метод передается менее конкретный тип .

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
woggles
источник
0

Дисперсия Co и Contra - довольно логичные вещи. Система языковых типов заставляет нас поддерживать логику реальной жизни. Это легко понять на примере.

ковариации

Например, вы хотите купить цветок, и у вас есть два магазина цветов в вашем городе: магазин роз и магазин ромашек.

Если вы спросите кого-то "где магазин цветов?" а кто-то скажет вам, где магазин роз, все будет в порядке? Да, потому что роза - это цветок, если вы хотите купить цветок, вы можете купить розу. То же самое относится, если кто-то ответил вам адресом магазина ромашки.

Это пример ковариации : вам разрешено гипс A<C>к A<B>, где Cэто подкласс B, если Aпроизводят общие значения (возвращают в качестве результата от функции). Ковариантность о производителях, поэтому C # использует ключевое словоout для ковариации.

Типы:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

Вопрос «где магазин цветов?», Ответ «магазин роз»:

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

контрвариация

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

Это пример контрвариации : вы позволили гипс A<B>к A<C>, где Cесть подкласс B, если Aистребляет общее значение. Contravariance касается потребителей, поэтому C # использует ключевое слово inдля контравариантности.

Типы:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

Вы рассматриваете свою девушку, которая любит любой цветок, как человека, который любит розы, и дарите ей розу:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

связи

VadzimV
источник