Мне сложно понять разницу между ковариацией и контравариантностью.
Разница между ковариацией и контр-дисперсией
Ответы (6)
Вопрос в том, «в чем разница между ковариацией и контравариантностью?»
Ковариация и контравариантность - это свойства функции отображения, которая связывает один член набора с другим. Более конкретно, отображение может быть ковариантным или контравариантным по отношению к отношению на этом множестве.
Рассмотрим следующие два подмножества множества всех типов 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>
.
In 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. Ковариация сохраняет направление присваиваемости. Контравариантность обращает его вспять.
IC<Animal> ⇒ IC<Tiger>
? Если у меня есть компаратор для Тигра, как я могу пройти через какое-либо животное? Вы также сказали, но устройство, которое может сравнивать двух Тигров, не обязательно может сравнивать любые два Животных.
- person Ashish Negi; 06.09.2016
IE<Tiger> ⇒ IE<Animal>
означает, что перечислимый объект Tiger может использоваться как перечислимый объект Animal в return type of a function
, а IC<Animal> ⇒ IC<Tiger>
означает, что Comparator of Animal может использоваться как Comparator of Tiger in input type of a function
.. контравариантность находится в типе ввода .. ковариация в типе возвращаемого значения ..
- person Ashish Negi; 06.09.2016
IEnumerable<Tiger>
в IEnumerable<Animal>
? Потому что нет возможности ввести жирафа в IEnumerable<Animal>
. Почему мы можем преобразовать IComparable<Animal>
в IComparable<Tiger>
? Потому что невозможно вытащить жирафа из IComparable<Animal>
. Есть смысл?
- person Eric Lippert; 06.09.2016
Наверное, проще всего привести примеры - я их точно так запомнил.
Ковариация
Канонические примеры: 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>
возвращаемый тип недействителен, как он может использовать T
в качестве вывода? Или это то, что он означает, потому что он не возвращает ничего, как вы можете видеть, что он никогда не может нарушить правило?
- person Alexander Derck; 25.02.2016
Я надеюсь, что мой пост поможет получить независимый от языка взгляд на тему.
Для наших внутренних тренингов я работал с замечательной книгой "Smalltalk, Objects and Design (Chamond Liu)" и перефразировал следующие примеры.
Что означает «последовательность»? Идея состоит в том, чтобы проектировать безопасные иерархии типов с легко заменяемыми типами. Ключом к достижению этой согласованности является соответствие на основе подтипа, если вы работаете со статически типизированным языком. (Мы обсудим здесь принцип замещения Лискова (LSP) на высоком уровне.)
Практические примеры (псевдокод / недействителен в C #):
Ковариация: Предположим, что птицы, откладывающие яйца «последовательно» со статической типизацией: если тип Bird кладет яйцо, не будет ли подтип Bird подтипом Egg? Например. типа «Утка кладет утиное яйцо», затем дается консистенция. Почему это согласуется? Потому что в таком выражении:
Egg anEgg = aBird.Lay();
ссылка aBird может быть юридически заменена на Bird или на экземпляр Duck. Мы говорим, что возвращаемый тип ковариантен типу, в котором определен Lay (). Переопределение подтипа может вернуть более специализированный тип. => «Они доставляют больше».Контравариантность: допустим, фортепьяно, на котором пианисты могут играть «последовательно» со статическим набором текста: если пианист играет на фортепиано, сможет ли он играть на рояле? Разве виртуоз не предпочтет играть на рояле? (Будьте осторожны, тут есть нюанс!) Это непоследовательно! Потому что в таком выражении:
aPiano.Play(aPianist);
aPiano не может быть юридически заменено пианино или экземпляром GrandPiano! На рояле может играть только виртуоз, пианисты - это слишком универсально! На роялях должны играть более общие типы, тогда игра будет последовательной. Мы говорим, что тип параметра контравариантен типу, в котором определен Play (). Переопределение подтипа может принимать более общий тип. => «Им нужно меньше».
Вернемся к C #:
Поскольку C # в основном является языком со статической типизацией, "местоположения" интерфейса типа, которые должны быть ко- или контравариантными (например, параметры и возвращаемые типы), должны быть явно отмечены, чтобы гарантировать последовательное использование / разработку этого типа, чтобы LSP работал нормально. В языках с динамической типизацией согласованность LSP обычно не является проблемой, другими словами, вы можете полностью избавиться от ко- и контравариантной «разметки» на интерфейсах и делегатах .Net, если вы используете только динамический тип в своих типах. - Но это не лучшее решение на C # (не стоит использовать динамические в публичных интерфейсах).
Вернемся к теории:
Описанное соответствие (ковариантные возвращаемые типы / контравариантные типы параметров) является теоретическим идеалом (поддерживается языками Emerald и POOL-1). Некоторые языки OOP (например, Eiffel) решили применить другой тип согласованности, особенно. также ковариантные типы параметров, потому что они лучше описывают реальность, чем теоретический идеал. В статически типизированных языках желаемая согласованность часто достигается применением шаблонов проектирования, таких как «двойная отправка» и «посетитель». Другие языки предоставляют так называемую «множественную отправку» или множественные методы (в основном это выбор перегрузок функций во время времени выполнения, например, с помощью CLOS) или получение желаемого эффекта с помощью динамической типизации.
Bird
определяет public abstract BirdEgg Lay();
, тогда Duck : Bird
ДОЛЖЕН реализовать public override BirdEgg Lay(){}
. Таким образом, ваше утверждение, что BirdEgg anEgg = aBird.Lay();
вообще имеет какую-либо вариацию, просто неверно. Поскольку это предпосылка сути объяснения, теперь вся суть утеряна. Могли бы вы вместо сказать, что ковариация существует внутри реализации, где DuckEgg неявно приводится к типу вывода / возврата BirdEgg? В любом случае, пожалуйста, проясните мое замешательство.
- person Suamere; 11.12.2015
DuckEgg Lay()
не является допустимым переопределением для Egg Lay()
в C #, и в этом суть. C # не поддерживает ковариантные возвращаемые типы, но Java и C ++ поддерживают. Я скорее описал теоретический идеал, используя синтаксис, подобный C #. В C # вам нужно позволить Bird и Duck реализовать общий интерфейс, в котором Lay определен как имеющий ковариантный тип возврата (то есть выход из спецификации), тогда вопросы совпадают!
- person Nico; 11.12.2015
extends
, Consumer super
.
- person Nico; 05.02.2020
Делегат преобразователя помогает мне понять разницу.
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();
Дисперсия 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
использует общее значение. Контравариантность касается потребителей, поэтому 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());
Ссылки
Представьте, что в организации есть две должности. Алиса - это стойка для стульев. И Боб - кладовщик таких же стульев.
Контравариантность. Теперь мы не можем назвать Боба кладовщиком мебели, потому что он не берет стол в свой магазин, он хранит только стулья. Но мы можем назвать его кладовщиком фиолетовых стульев, потому что фиолетовый - это стул. Это IBookkeeper<in T>
, мы разрешаем присваивать более конкретные типы, а не менее. in
означает потоки данных в объект.
Covarinace. Напротив, мы можем назвать Алису прилавком мебели, потому что это не повлияет на ее роль. Но мы не можем назвать ее счетчиком красных стульев, потому что мы ожидаем, что она не считает не красные стулья, а она их считает. Это ICounter<out T>
, разрешить неявное преобразование в менее конкретное, а не в более конкретное. out
означает потоки данных из объекта.
А инвариантность - это когда мы не можем сделать и то, и другое.