Разница между ковариацией и контрастностью - PullRequest
143 голосов
/ 02 февраля 2010

Мне трудно понять разницу между ковариацией и контравариантностью.

Ответы [ 5 ]

254 голосов
/ 04 февраля 2010

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

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

Рассмотрим следующие два подмножества множества всех типов C #. Во-первых:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

И, во-вторых, это явно связанный набор:

{ IEnumerable<Animal>, 
  IEnumerable<Tiger>, 
  IEnumerable<Fruit>, 
  IEnumerable<Banana> }

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

105 голосов
/ 02 февраля 2010

Вероятно, легче всего привести примеры - это, конечно, то, как я их помню.

ковариации

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

14 голосов
/ 15 июля 2011

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

Для наших внутренних тренингов я работал с замечательной книгой "Smalltalk, Objects and Design (Chamond Liu)" и перефразировал следующие примеры.

Что означает «согласованность»? Идея состоит в том, чтобы спроектировать безопасные с точки зрения типов иерархии типов с легко заменяемыми типами. Ключом к получению этой согласованности является соответствие на основе подтипов, если вы работаете на языке со статической типизацией. (Мы обсудим принцип замены Лискова (LSP) здесь на высоком уровне.)

Практические примеры (псевдокод / ​​недействительный в C #):

  • Ковариантность: Давайте предположим, что птицы, которые откладывают яйца «последовательно» со статической типизацией: если тип Bird откладывает яйцо, не подтип Bird подкладывает подтип яйца? Например. тип Duck откладывает DuckEgg, затем задается последовательность. Почему это соответствует? Потому что в таком выражении: Egg anEgg = aBird.Lay(); ссылка aBird может быть юридически заменена Bird или экземпляром Duck. Мы говорим, что возвращаемый тип ковариантен типу, в котором определен Lay (). Переопределение подтипа может возвращать более специализированный тип. => «Они доставляют больше».

  • Контравариантность. Предположим, пианино, что пианисты могут играть «последовательно» со статической типизацией: если пианист играет на пианино, сможет ли она играть на рояле? Не хотел бы Виртуоз играть на рояле? (Будьте предупреждены; есть поворот!) Это противоречиво! Потому что в таком выражении: aPiano.Play(aPianist); aPiano не может быть юридически заменено Piano или экземпляром GrandPiano! Играть на рояле может только виртуоз, пианисты слишком общие! GrandPianos должны воспроизводиться более общими типами, тогда игра будет последовательной. Мы говорим, что тип параметра противоположен типу, в котором определяется Play (). Переопределение подтипа может принимать более обобщенный тип. => «Они требуют меньше».

Вернуться к C #:
Поскольку C # в основном является языком со статической типизацией, «местоположения» интерфейса типа, которые должны быть ко-или контравариантными (например, параметры и возвращаемые типы), должны быть помечены явно, чтобы гарантировать последовательное использование / развитие этого типа, чтобы сделать LSP работает нормально. В динамически типизированных языках согласованность LSP обычно не является проблемой, другими словами, вы могли бы полностью избавиться от ко-и контравариантной «разметки» на интерфейсах и делегатах .Net, если бы вы использовали тип динамический только в ваших типах. - Но это не лучшее решение в C # (вы не должны использовать динамические в публичных интерфейсах).

Вернуться к теории:
Описанное соответствие (ковариантные возвращаемые типы / контравариантные типы параметров) является теоретическим идеалом (поддерживаемым языками Emerald и POOL-1). Некоторые языки oop (например, Eiffel) решили применить другой тип согласованности, особенно также ковариантные типы параметров, потому что он лучше описывает реальность, чем теоретический идеал. В статически типизированных языках желаемая согласованность часто должна достигаться путем применения шаблонов проектирования, таких как «двойная диспетчеризация» и «посетитель». Другие языки предоставляют так называемые «множественные диспетчеризации» или множественные методы (это в основном выбор перегрузок функций при времени выполнения , например, с CLOS) или получение желаемого эффекта с помощью динамической типизации.

4 голосов
/ 04 октября 2015

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

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

3 голосов
/ 06 октября 2017

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

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();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...