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

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


person jane doe    schedule 02.02.2010    source источник


Ответы (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. Ковариация сохраняет направление присваиваемости. Контравариантность обращает его вспять.

person Eric Lippert    schedule 04.02.2010
comment
Для кого-то вроде меня было бы лучше добавить примеры, показывающие, что НЕ является ковариантным, а что НЕ контравариантным, а что НЕ одновременно. - person bjan; 29.07.2015
comment
Спасибо, Эрик, похоже, такое же, как у java ‹? расширяет T ›и‹? супер Т ›. - person Bargitta; 09.03.2016
comment
@Bargitta: Это очень похоже. Разница в том, что C # использует определенную дисперсию сайта, а Java использует дисперсию сайта вызова. То, как все меняется, одинаково, но разработчик говорит, что мне нужно, чтобы это был вариант, отличается. Между прочим, функция на обоих языках была частично разработана одним и тем же человеком! - person Eric Lippert; 09.03.2016
comment
Ах я вижу. вот почему в java у нас есть продюсер расширений, потребительский супер - person Bargitta; 11.03.2016
comment
можешь объяснить, как работает IC<Animal> ⇒ IC<Tiger> ? Если у меня есть компаратор для Тигра, как я могу пройти через какое-либо животное? Вы также сказали, но устройство, которое может сравнивать двух Тигров, не обязательно может сравнивать любые два Животных. - person Ashish Negi; 06.09.2016
comment
@AshishNegi: Прочтите стрелку, которая может использоваться как. То, что может сравнивать животных, может использоваться как вещь, которая может сравнивать тигров. Теперь имеет смысл? - person Eric Lippert; 06.09.2016
comment
хорошо .. так что 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
comment
@AshishNegi: Нет, это не так. IEnumerable является ковариантным, потому что T появляется только в возвращаемых значениях методов IEnumerable. И IComparable является контравариантным, потому что T появляется только как формальные параметры методов IComparable. - person Eric Lippert; 06.09.2016
comment
@AshishNegi: Вы хотите подумать о логических причинах, лежащих в основе этих отношений. Почему мы можем безопасно преобразовать IEnumerable<Tiger> в IEnumerable<Animal>? Потому что нет возможности ввести жирафа в IEnumerable<Animal>. Почему мы можем преобразовать IComparable<Animal> в IComparable<Tiger>? Потому что невозможно вытащить жирафа из IComparable<Animal>. Есть смысл? - person Eric Lippert; 06.09.2016
comment
имеет смысл .. раньше я смотрел с точки зрения других fns .. вы объяснили, что нужно смотреть с точки зрения fns IEnumerable и IComparable .. - person Ashish Negi; 07.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> меняет положение, если вы понимаете, что я имею в виду. Это «выход» в том смысле, что значения могут передаваться от реализации метода к коду вызывающего, точно так же, как это может возвращаемое значение. К счастью, обычно такие вещи не всплывают :)

person Jon Skeet    schedule 02.02.2010
comment
Для кого-то вроде меня было бы лучше добавить примеры, показывающие, что НЕ является ковариантным, а что НЕ контравариантным, а что НЕ одновременно. - person bjan; 29.07.2015
comment
@Jon Skeet Хороший пример, я только не понимаю параметр типа Action<T> по-прежнему использует только T в позиции вывода. Action<T> возвращаемый тип недействителен, как он может использовать T в качестве вывода? Или это то, что он означает, потому что он не возвращает ничего, как вы можете видеть, что он никогда не может нарушить правило? - person Alexander Derck; 25.02.2016
comment
Для себя в будущем, который возвращается к этому отличному ответу снова, чтобы заново понять разницу, это именно та строка, которую вы хотите: [Ковариация] работает, потому что если вы только извлекаете ценности из API, и он будет возвращать что-то конкретное (например, строку), вы можете рассматривать это возвращаемое значение как более общий тип (например, объект). - person Matt Klein; 27.01.2017
comment
Самая запутанная часть всего этого заключается в том, что для ковариации или контравариантности, если вы игнорируете направление (внутрь или наружу), вы в любом случае получите более конкретное преобразование в более общее! Я имею в виду: вы можете рассматривать это возвращаемое значение как более общий тип (например, объект) для ковариации и: API ожидает чего-то общего (например, объекта), вы можете дать ему что-то более конкретное (например, строка) для контравариантность. Для меня это звучит одинаково! - person XMight; 06.03.2017
comment
@AlexanderDerck: Не знаю, почему я не ответил тебе раньше; Согласен, это непонятно, постараюсь прояснить. - person Jon Skeet; 06.03.2017

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

Для наших внутренних тренингов я работал с замечательной книгой "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) или получение желаемого эффекта с помощью динамической типизации.

person Nico    schedule 14.07.2011
comment
Вы говорите, что переопределение подтипа может вернуть более специализированный тип. Но это совершенно неверно. Если Bird определяет public abstract BirdEgg Lay();, тогда Duck : Bird ДОЛЖЕН реализовать public override BirdEgg Lay(){}. Таким образом, ваше утверждение, что BirdEgg anEgg = aBird.Lay(); вообще имеет какую-либо вариацию, просто неверно. Поскольку это предпосылка сути объяснения, теперь вся суть утеряна. Могли бы вы вместо сказать, что ковариация существует внутри реализации, где DuckEgg неявно приводится к типу вывода / возврата BirdEgg? В любом случае, пожалуйста, проясните мое замешательство. - person Suamere; 11.12.2015
comment
Короче: вы правы! Извините за путаницу. DuckEgg Lay() не является допустимым переопределением для Egg Lay() в C #, и в этом суть. C # не поддерживает ковариантные возвращаемые типы, но Java и C ++ поддерживают. Я скорее описал теоретический идеал, используя синтаксис, подобный C #. В C # вам нужно позволить Bird и Duck реализовать общий интерфейс, в котором Lay определен как имеющий ковариантный тип возврата (то есть выход из спецификации), тогда вопросы совпадают! - person Nico; 11.12.2015
comment
В качестве аналога комментария Мэтта-Кляйна к ответу @Jon-Skeet моему будущему «я»: лучший вывод для меня - они доставляют больше (конкретных) и требуют меньше (конкретных). Меньше требуй - больше доставляй - отличная мнемоника! Это аналогично работе, в которой я надеюсь потребовать менее конкретных инструкций (общие запросы) и все же предоставить что-то более конкретное (реальный рабочий продукт). В любом случае порядок подтипов (LSP) не нарушается. - person karfus; 31.07.2019
comment
@karfus: Спасибо, но насколько я помню, я перефразировал идею «Требовать меньше, а доставлять больше из другого источника». Может быть, это была книга Лю, о которой я упоминал выше ... или даже доклад .NET Rock. Кстати. в Java люди сократили мнемонику до PECS, который напрямую относится к синтаксическому способу объявления отклонений, PECS - для Producer 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();
person woggles    schedule 06.10.2017

Дисперсия 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());

Ссылки

person VadzimV    schedule 12.10.2019

Представьте, что в организации есть две должности. Алиса - это стойка для стульев. И Боб - кладовщик таких же стульев.

Контравариантность. Теперь мы не можем назвать Боба кладовщиком мебели, потому что он не берет стол в свой магазин, он хранит только стулья. Но мы можем назвать его кладовщиком фиолетовых стульев, потому что фиолетовый - это стул. Это IBookkeeper<in T>, мы разрешаем присваивать более конкретные типы, а не менее. in означает потоки данных в объект.

Covarinace. Напротив, мы можем назвать Алису прилавком мебели, потому что это не повлияет на ее роль. Но мы не можем назвать ее счетчиком красных стульев, потому что мы ожидаем, что она не считает не красные стулья, а она их считает. Это ICounter<out T>, разрешить неявное преобразование в менее конкретное, а не в более конкретное. out означает потоки данных из объекта.

А инвариантность - это когда мы не можем сделать и то, и другое.

person Artyom    schedule 03.06.2021