Мне трудно понять разницу между ковариацией и контравариантностью.
Разница между ковариацией и противоречием
Ответ 1
Вопрос: "В чем разница между ковариацией и контравариантностью?"
Ковариация и контравариантность - это свойства функции отображения, которая связывает один элемент множества с другим. Более конкретно, отображение может быть ковариантным или контравариантным относительно отношения на этом множестве.
Рассмотрим следующие два подмножества множества всех типов С#. Во-первых:
{ 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
В С# 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>
.
В С# 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>
.
Отображение, которое сохраняет, но меняет обратное отношение, называется контравариантным отображением.
Опять же, это должно быть четко правильным. Устройство, которое может сравнивать двух животных, также может сравнивать двух тигров, но устройство, которое может сравнивать двух тигров, не обязательно может сравнивать любые два Животные.
Итак, что разница между ковариацией и контравариантностью в С# 4. Ковариация сохраняет направление назначения. Контравариантность меняет его.
Ответ 2
Наверное, проще всего привести примеры - конечно, как я их помню.
ковариация
Канонические примеры: 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>
поворачивает его, если вы понимаете, что я имею в виду. Это "выход" в том смысле, что значения могут перейти от реализации метода к коду вызывающего абонента, точно так же, как и возвращаемое значение. К счастью, обычно такого рода вещи не приходят:)
Ответ 3
Надеюсь, мой пост поможет получить языковой взгляд на тему.
Для наших внутренних тренировок я работал с замечательной книгой "Smalltalk, Objects and Design (Chamond Liu)", и я перефразировал следующие примеры.
Что означает "последовательность"? Идея состоит в том, чтобы проектировать иерархии типа типа с сильно замещаемыми типами. Ключом к получению этой согласованности является соответствие типа субтипа, если вы работаете на статически типизированном языке. (Здесь мы обсудим Принцип замещения Лискова (LSP) на высоком уровне.)
Практические примеры (псевдокод/недопустимый в С#):
-
Ковариация. Предположим, что птицы, которые кладут яйца "последовательно" со статической типизацией: если тип Bird закладывает яйцо, не будет ли подтип птиц подтипом яйца? Например. тип Duck содержит DuckEgg, тогда задается согласованность. Почему это непротиворечиво? Потому что в таком выражении:
Egg anEgg = aBird.Lay();
ссылка aBird может быть юридически заменена птицей или экземпляром утки. Мы говорим, что тип возврата ковариант к типу, в котором определено Lay(). Приоритет подтипа может возвращать более специализированный тип. = > "Они доставляют больше". -
Контравариантность: предположим, что пианисты могут играть "последовательно" со статической типизацией: если пианист играет на фортепиано, сможет ли она сыграть Гран-Пиано? Не будет ли виртуоз играть Гран-Пиано? (Будьте осторожны, есть поворот!) Это непоследовательно! Потому что в таком выражении:
aPiano.Play(aPianist);
aPiano не может быть юридически заменен фортепиано или экземпляром GrandPiano! "GrandPiano" может играть только виртуоз, пианисты слишком общие! GrandPianos должен воспроизводиться более общими типами, тогда игра последовательна. Мы говорим, что тип параметра контравариантен типу, в котором определено Play(). Подстановочное подтип может принимать более обобщенный тип. = > "Они требуют меньше".
Назад к С#:
Поскольку С# - это, в основном, статически типизированный язык, "местоположения" интерфейса типа, которые должны быть согласованы или контравариантны (например, параметры и типы возвращаемых данных), должны быть отмечены явно, чтобы гарантировать последовательное использование/разработку этого типа, чтобы сделать LSP работает нормально. В динамически типизированных языках согласованность LSP обычно не является проблемой, другими словами, вы можете полностью избавиться от совместной и контравариантной "разметки" на .Net-интерфейсах и делегатах, если вы использовали только динамический тип в своих типах. - Но это не лучшее решение в С# (вы не должны использовать динамический интерфейс в общедоступных интерфейсах).
Назад к теории:
Описанное соответствие (ковариантные типы возврата/контравариантные типы параметров) является теоретическим идеалом (поддерживается языками Emerald и POOL-1). Некоторые языки OOP (например, Eiffel) решили применить другой тип согласованности, особенно. также ковариантные типы параметров, поскольку он лучше описывает реальность, чем теоретический идеал.
В статически типизированных языках желаемая консистенция часто достигается путем применения шаблонов проектирования, таких как "двойная диспетчеризация" и "посетитель". Другие языки предоставляют так называемую "множественную отправку" или несколько методов (это в основном выбор перегрузок функций во время выполнения, например, с помощью CLOS) или получение желаемого эффекта с помощью динамического набора.
Ответ 4
Делегат конвертера помогает мне понять разницу.
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();
Ответ 5
Если вы хотите назначить какой-либо метод для делегата, подпись метода должна точно соответствовать подписи делегата. Сказав это, ковариация и контравариантность позволяют проявлять некоторую степень гибкости при сопоставлении сигнатуры методов с делегатами.
Вы можете обратиться к этой статье чтобы понять ковариацию, контравариантность и различия между ними.
Ответ 6
Лучше, если я объясню, почему ковариация возвращаемого типа и контравариантность входных параметров не разрешены в С#.
Хирург, BabySitter и Plumber происходят из класса Person
public class Surgeon : Person { }
public class Plumber : Person { }
public class BabySitter : Person { }
public class Person { }
// We want to restrict performing surgeries only to doctors.
public void PerformSurgery(Surgeon doctor)
{
// If we pass in a Person to PerformSurgery he/she might be a plumber
// and we don't want plumbers performing surgeries
doctor.Operate();
}
// This also makes sense for fixing pipeleaks
// If we pass in a Person to FixPipeLeak he/she might be a babysitter
public void FixPipeLeak(Plumber plumber)
{
plumber.FixPlumbing();
}
PerformSurgery(new Person())
и FixPipeLeak(new Person())
генерируют ошибку компиляции.
Поэтому мы не хотим, чтобы контравариантность в наших входных параметрах
Что мы хотим
public Person FindNewFriend()
{
return new Surgeon();
//or
return new Plumber();
// or
return new BabySitter();
}
Нам все равно, является ли человек врачом или водопроводчиком или няней, мы просто хотим выпить пива и хорошо провести время Поэтому мы хотим поддерживать контравариантность в наших возвращаемых типах. Это успешно компилируется
Что мы не хотим
public BabySitter FindSomeoneToBabySit()
{
// We don't want just anybody to watch our child
// We want an actual babysitter
return new Person();
// This generates a compile error
// What we want
return new BabySitter();
}
Поэтому мы не хотим ковариации возвращаемого типа
Ответ 7
Дисперсия Co и Contra - довольно логичные вещи. Система языковых типов заставляет нас поддерживать логику реальной жизни. Это легко понять на примере.
ковариации
Например, вы хотите купить цветок, и у вас есть два магазина цветов в вашем городе: магазин роз и магазин ромашек.
Если вы спросите кого-то "где магазин цветов?" а кто-то скажет вам, где магазин роз, все будет в порядке? Да, потому что роза - это цветок, если вы хотите купить цветок, вы можете купить розу. То же самое относится, если кто-то ответил вам адресом магазина ромашки.
Это пример ковариации: вам разрешено приводить A<C>
к A<B>
, где C
является подклассом B
, если A
создает общие значения (возвращается в результате функция). Ковариантность касается производителей, поэтому С# использует ключевое слово 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 касается потребителей, поэтому С# использует ключевое слово 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());