У мене виникають проблеми з розумінням різниці між коваріацією та протирічністю.
У мене виникають проблеми з розумінням різниці між коваріацією та протирічністю.
Відповіді:
Питання в тому, "в чому різниця між коваріацією та протирічністю?"
Коваріація та противаріантність - властивості функції відображення, яка асоціює одного члена множини з іншим . Більш конкретно, таке відображення може бути коваріантним або контраваріантним по відношенню до відношенню з цього набору.
Розглянемо наступні два підмножини набору всіх типів 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. Коваріація зберігає напрям призначення. Суперечливість його перевертає .
IEnumerable<Tiger>
можемо IEnumerable<Animal>
безпечно перетворитись ? Тому що немає способу ввести жирафа в IEnumerable<Animal>
. Чому ми можемо перетворити IComparable<Animal>
на IComparable<Tiger>
? Тому що немає можливості зняти жирафа з IComparable<Animal>
. Мати сенс?
Напевно, найпростіше наводити приклади - це, звичайно, я їх пам’ятаю.
Коваріація
Канонічні приклади: 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
як вихід? Або це те, що це означає, тому що він не повертає нічого, що ви можете бачити, що він ніколи не може порушити правило?
Я сподіваюся, що моя публікація допомагає отримати мовно-агностичний погляд на цю тему.
Для наших внутрішніх тренінгів я працював із чудовою книгою "Маленькі розмови, об'єкти та дизайн (Шамонд Лю") ", і я перефразував наступні приклади.
Що означає "послідовність"? Ідея полягає у розробці безпечних для ієрархій типів типів з високозамінними типами. Ключовим фактором для отримання цієї консистенції є відповідність підтипу, якщо ви працюєте статично введеною мовою. (Тут ми обговоримо Принцип заміщення Ліскова (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? У будь-якому випадку, будь ласка, очистіть мою плутанину.
DuckEgg Lay()
не є дійсним перевизначенням для Egg Lay()
C # , і в цьому суть. C # не підтримує коваріантні типи повернення, але Java, а також C ++. Я скоріше описав теоретичний ідеал, використовуючи синтаксис C-подібний. У C # вам потрібно дозволити Bird and Duck реалізувати загальний інтерфейс, в якому Lay визначено, щоб він мав коваріантний тип повернення (тобто специфікація), тоді питання поєднуються разом!
extends
, Споживач super
".
Делегат перетворювача допомагає мені зрозуміти різницю.
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());