Різниця між коваріацією та контра-дисперсією


Відповіді:


266

Питання в тому, "в чому різниця між коваріацією та протирічністю?"

Коваріація та противаріантність - властивості функції відображення, яка асоціює одного члена множини з іншим . Більш конкретно, таке відображення може бути коваріантним або контраваріантним по відношенню до відношенню з цього набору.

Розглянемо наступні два підмножини набору всіх типів C #. Перший:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

І друге, це чітко пов'язаний набір:

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

Існує операція зі картографуванням від першого набору до другого набору. Тобто для кожного Т у першому множині відповідний тип у другому наборі 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
Для когось, як я, було б краще додати приклади, що показують, що НЕ є коваріантом, а що НЕ є противаріантним, а що НЕ обом.
bjan

2
@Bargitta: Це дуже схоже. Різниця полягає в тому, що C # використовує певну дисперсію сайту, а Java використовує дисперсію сайту для викликів . Тож те, як все змінюється, однакове, але там, де розробник каже "мені потрібно, щоб це було варіантом", є іншим. До речі, функція в обох мовах почасти була розроблена однією і тією ж людиною!
Ерік Ліпперт

2
@AshishNegi: Прочитайте стрілку як "може використовуватися як". "Реч, яка може порівнювати тварин, може використовуватися як річ, яка може порівнювати тигрів". Маєте сенс зараз?
Ерік Ліпперт

1
@AshishNegi: Ні, це неправильно. IEnumerable є ковариантним, оскільки T з'являється лише у зворотах методів IEnumerable. І ICпорівнянний є протилежним, оскільки 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
Для когось, як я, було б краще додати приклади, що показують, що НЕ є коваріантом, а що НЕ є противаріантним, а що НЕ обом.
bjan

1
@ Jon Skeet Гарний приклад, я лише не розумію "параметр типу Action<T>все ще використовується лише Tу вихідному положенні" . Action<T>тип повернення недійсний, як його можна використовувати Tяк вихід? Або це те, що це означає, тому що він не повертає нічого, що ви можете бачити, що він ніколи не може порушити правило?
Олександр Дерк

2
Моєму майбутньому Я, який знову повертається до цієї чудової відповіді, щоб знову дізнатися про різницю, це такий рядок, який ви хочете: "[Коваріація] працює, тому що якщо ви лише виймаєте значення з API, і він поверне щось конкретний (як рядок), ви можете трактувати це повернене значення як більш загальний тип (як об’єкт). "
Метт Кляйн

Найбільш заплутаною частиною всього цього є те, що або для коваріації, або для протиріччя, якщо ви ігноруєте напрямок (вхід чи вихід), ви все одно отримаєте більш специфічне для більш загального перетворення! Я маю на увазі: "ви можете розглядати це повернене значення як більш загальний тип (як об'єкт)" для коваріації та: "API очікує чогось загального (наприклад, об'єкта), ви можете надати йому щось більш конкретне (наприклад, рядок)" для протиріччя . Для мене ці звуки такі самі!
XMight

@AlexanderDerck: Не впевнений, чому я раніше не відповідав тобі; Я погоджуюся, що це незрозуміло, і спробую це уточнити.
Джон Скіт

16

Я сподіваюся, що моя публікація допомагає отримати мовно-агностичний погляд на цю тему.

Для наших внутрішніх тренінгів я працював із чудовою книгою "Маленькі розмови, об'єкти та дизайн (Шамонд Лю") ", і я перефразував наступні приклади.

Що означає "послідовність"? Ідея полягає у розробці безпечних для ієрархій типів типів з високозамінними типами. Ключовим фактором для отримання цієї консистенції є відповідність підтипу, якщо ви працюєте статично введеною мовою. (Тут ми обговоримо Принцип заміщення Ліскова (LSP) на високому рівні.)

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

  • Коваріантність: Припустимо, птахи, які відкладають яйця «послідовно» зі статичним набором тексту: Якщо тип Птах відкладає яйце, чи не став би підтип Птаха підтипом Яйце? Наприклад, тип Качка відкладає DuckEgg, тоді надається консистенція. Чому це послідовно? Тому що в такому виразі: Egg anEgg = aBird.Lay();посилання aBird може бути юридично заміщена Bird або екземпляром Duck. Ми кажемо, що тип повернення є ковариантним до типу, у якому визначено Lay (). Перевизначення підтипу може повернути більш спеціалізований тип. => "Вони надають більше."

  • Суперечність: Припустимо, на фортепіано, що піаністи можуть грати «послідовно» зі статичним набором тексту: Якщо піаністка грає на фортепіано, чи змогла б вона грати на GrandPiano? Чи не скоріше віртуоз грав у GrandPiano? (Будьте попереджені; є поворот!) Це непослідовно! Тому що в такому виразі: aPiano.Play(aPianist);aPiano не може бути юридично замінений Піаніно або екземпляром GrandPiano! У GrandPiano може грати тільки віртуоз, піаністи занадто загальні! GrandPianos повинен бути відтворений більш загальними типами, тоді гра є послідовною. Ми говоримо, що тип параметра протилежний типу, в якому визначено Play (). Перевизначення підтипу може прийняти більш узагальнений тип. => "Їм потрібно менше".

Назад до C #:
Оскільки C # є в основному статично введеною мовою, "розташування" інтерфейсу типу, який повинен бути ко- або противаріантним (наприклад, параметри та типи повернення), повинні бути чітко позначені, щоб гарантувати послідовне використання / розвиток цього типу. , щоб LSP працював нормально. У динамічно набраних мовах послідовність LSP, як правило, не є проблемою, іншими словами, ви можете повністю позбутися ко-та противаріантної "розмітки" на .Net інтерфейсах та делегатах, якби тільки використовували динаміку типу у своїх типах. - Але це не найкраще рішення в C # (не слід використовувати динамічні в публічних інтерфейсах).

Повернення до теорії:
Описана відповідність (типи повернення коваріантів / типи контраваріантів) є теоретичним ідеалом (підтримується мовами Emerald та POOL-1). Деякі мови oop (наприклад, Ейфель) вирішили застосувати інший тип консистенції, esp. також типи параметрів коваріанта, оскільки він краще описує реальність, ніж теоретичний ідеал. Статично набраними мовами бажана послідовність часто повинна бути досягнута шляхом застосування моделей дизайну, таких як "подвійне відправлення" та "відвідувач". Інші мови пропонують так звану "багаторазову диспетчеризацію" або декілька методів (це, в основному, вибір перевантажень функції під час виконання , наприклад, з 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 and Duck реалізувати загальний інтерфейс, в якому Lay визначено, щоб він мав коваріантний тип повернення (тобто специфікація), тоді питання поєднуються разом!
Ніко

1
Як аналог коментаря Метта-Кляйна на відповідь @ Jon-Skeet, "на моє майбутнє": "Найкраще для мене тут -" Вони доставляють більше "(конкретно) та" Їм потрібно менше "(конкретно). "Вимагайте менше і доставляйте більше" - це чудова мнемоніка! Це аналогічно роботі, де я сподіваюся вимагати менш конкретні вказівки (загальні запити) і все-таки доставити щось більш конкретне (фактичний робочий продукт). У будь-якому випадку порядок підтипів (LSP) не порушений.
karfus

@karfus: Дякую, але, як я пам’ятаю, я перефразовував ідею «Потрібно менше і доставити більше» з іншого джерела. Можливо, це була книга Лю, яку я згадую вище ... або навіть розмова .NET Rock. Btw. на Яві люди звели мнемонічне значення до "PECS", що безпосередньо стосується синтаксичного способу оголошення дисперсій, PECS - для "Виробник extends, Споживач 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();

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споживає загальне значення. Протиріччя стосується споживачів, тому 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());

Посилання

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.