Różnica między kowariancją a kontrawariancją

Odpowiedzi:

266

Pytanie brzmi: „jaka jest różnica między kowariancją a kontrawariancją?”

Kowariancja i kontrawariancja to właściwości funkcji odwzorowującej, która kojarzy jeden element zestawu z innym . Dokładniej, odwzorowanie może być kowariantne lub kontrawariantne w odniesieniu do relacji w tym zbiorze.

Rozważmy następujące dwa podzbiory zestawu wszystkich typów C #. Pierwszy:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

Po drugie, ten jasno powiązany zestaw:

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

Istnieje operacja mapowania z pierwszego zestawu do drugiego zestawu. Oznacza to, że dla każdego T w pierwszym zestawie odpowiada typowi w drugim zestawie IEnumerable<T>. Lub, w skrócie, mapowanie to T → IE<T>. Zauważ, że jest to „cienka strzałka”.

Ze mną do tej pory?

Rozważmy teraz relację . Istnieje relacja zgodności przypisania między parami typów w pierwszym zestawie. Wartość typu Tigermoże być przypisana do zmiennej typu Animal, więc te typy są określane jako „zgodne z przypisaniem”. Write Chodźmy „wartość typu Xmoże być przypisana do zmiennej typu Y” w krótszej formie: X ⇒ Y. Zauważ, że jest to „gruba strzała”.

Tak więc w naszym pierwszym podzbiorze są wszystkie relacje zgodności przypisań:

Tiger   Tiger
Tiger   Animal
Animal  Animal
Banana  Banana
Banana  Fruit
Fruit   Fruit

W języku C # 4, który obsługuje kowariantną zgodność przypisań niektórych interfejsów, istnieje relacja zgodności przypisania między parami typów w drugim zestawie:

IE<Tiger>   IE<Tiger>
IE<Tiger>   IE<Animal>
IE<Animal>  IE<Animal>
IE<Banana>  IE<Banana>
IE<Banana>  IE<Fruit>
IE<Fruit>   IE<Fruit>

Zauważ, że mapowanie T → IE<T> zachowuje istnienie i kierunek zgodności przypisań . To znaczy, jeśli X ⇒ Y, to też jest prawdą IE<X> ⇒ IE<Y>.

Jeśli mamy dwie rzeczy po obu stronach grubej strzały, możemy zastąpić obie strony czymś po prawej stronie odpowiedniej cienkiej strzały.

Odwzorowanie, które ma tę właściwość w odniesieniu do konkretnej relacji, nazywane jest „mapowaniem kowariantnym”. To powinno mieć sens: sekwencja Tygrysów może być użyta tam, gdzie potrzebna jest sekwencja Zwierząt, ale odwrotnie nie jest prawdą. Niekoniecznie można użyć sekwencji zwierząt, gdy potrzebna jest sekwencja Tygrysów.

To jest kowariancja. Rozważmy teraz ten podzbiór zbioru wszystkich typów:

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

teraz mamy mapowanie z pierwszego zestawu do trzeciego zestawu T → IC<T>.

W 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>

Oznacza to, że mapowanie T → IC<T>został zachowany istnienie ale odwrócony kierunek kompatybilności przypisania. To znaczy, jeśli X ⇒ Ywtedy IC<X> ⇐ IC<Y>.

Odwzorowanie, które zachowuje relację, ale ją odwraca, nazywa się odwzorowaniem kontrawariantnym .

Powinno to być wyraźnie poprawne. Urządzenie, które może porównać dwa Zwierzęta, może również porównać dwa Tygrysy, ale urządzenie, które może porównać dwa Tygrysy, niekoniecznie może porównać dwa Zwierzęta.

To jest różnica między kowariancją a kontrawariancją w języku C # 4. Kowariancja zachowuje kierunek przypisywalności. Kontrawariancja ją odwraca .

Eric Lippert
źródło
4
Dla kogoś takiego jak ja byłoby lepiej dodać przykłady pokazujące, co NIE jest kowariantne i co NIE jest kontrawariantne, a co NIE jest jednym i drugim.
bjan
2
@Bargitta: Jest bardzo podobnie. Różnica polega na tym, że C # używa określonej wariancji serwisu, a Java używa wariancji serwisu wywołań . Tak więc sytuacja jest taka sama, ale sytuacja, w której programista mówi „Potrzebuję tego wariantu”, jest inna. Nawiasem mówiąc, funkcja w obu językach została częściowo zaprojektowana przez tę samą osobę!
Eric Lippert
2
@AshishNegi: Przeczytaj strzałkę jako „może być używany jako”. „Rzecz, która może porównywać zwierzęta, może być używana jako rzecz, która może porównywać tygrysy”. Czy teraz ma sens?
Eric Lippert
1
@AshishNegi: Nie, to nie w porządku. IEnumerable jest kowariantna, ponieważ T pojawia się tylko w zwrotach metod IEnumerable. I IComparable jest kontrawariantne, ponieważ T pojawia się tylko jako formalne parametry metod IComparable .
Eric Lippert,
2
@AshishNegi: Chcesz pomyśleć o logicznych powodach leżących u podstaw tych relacji. Dlaczego możemy przekształcić IEnumerable<Tiger>się IEnumerable<Animal>bezpiecznie? Ponieważ nie ma sposobu, aby wprowadzić żyrafę do IEnumerable<Animal>. Dlaczego możemy przekonwertować IComparable<Animal>na IComparable<Tiger>? Ponieważ nie ma sposobu, aby wyjąć żyrafę z pliku IComparable<Animal>. Ma sens?
Eric Lippert,
111

Chyba najłatwiej podać przykłady - na pewno tak je pamiętam.

Kowariancja

Przykłady: kanoniczne IEnumerable<out T>,Func<out T>

Możesz konwertować z IEnumerable<string>na IEnumerable<object>lub Func<string>na Func<object>. Wartości wychodzą tylko z tych obiektów.

Działa, ponieważ jeśli pobierasz wartości tylko z interfejsu API i zwraca coś konkretnego (na przykład string), możesz traktować tę zwróconą wartość jako typ bardziej ogólny (na przykład object).

Sprzeczność

Przykłady: kanoniczne IComparer<in T>,Action<in T>

Możesz konwertować z IComparer<object>do IComparer<string>lub Action<object>do Action<string>; wartości trafiają tylko do tych obiektów.

Tym razem działa, ponieważ jeśli API oczekuje czegoś ogólnego (np. object), Możesz nadać mu coś bardziej szczegółowego (np string.).

Bardziej ogólnie

Jeśli masz interfejs IFoo<T>, może być kowariantny w T(tj. Zadeklarować go tak, IFoo<out T>jakby Tbył używany tylko w pozycji wyjściowej (np. Typ powrotu) w interfejsie. Może być kontrawariantny w T(tj. IFoo<in T>), Jeśli Tjest używany tylko w pozycji wejściowej ( np. typ parametru).

Jest to potencjalnie mylące, ponieważ „pozycja wyjściowa” nie jest tak prosta, jak się wydaje - parametr typu Action<T>jest nadal używany tylko Tw pozycji wyjściowej - kontrawariancja Action<T>obraca go w kółko, jeśli rozumiesz, o co mi chodzi. Jest to „wyjście”, w którym wartości mogą być przekazywane z implementacji metody do kodu wywołującego, podobnie jak wartość zwracana. Na szczęście zwykle takie rzeczy się nie pojawiają :)

Jon Skeet
źródło
1
Dla kogoś takiego jak ja byłoby lepiej dodać przykłady pokazujące, co NIE jest kowariantne i co NIE jest kontrawariantne, a co NIE jest jednym i drugim.
bjan
1
@Jon Skeet Ładny przykład, nie rozumiem tylko, że „parametr typu Action<T>jest nadal używany tylko Tw pozycji wyjściowej” . Action<T>zwracany typ jest void, w jaki sposób można go używać Tjako danych wyjściowych? A może właśnie to oznacza, ponieważ nie zwraca niczego, co widać, że nigdy nie może naruszyć reguły?
Alexander Derck,
2
Mojemu przyszłemu ja, które wraca do tej doskonałej odpowiedzi ponownie, aby ponownie nauczyć się różnicy, oto linia, której chcesz: „[Kowariancja] działa, ponieważ jeśli pobierasz wartości tylko z API, to coś zwróci specific (jak string), możesz traktować tę zwróconą wartość jako typ bardziej ogólny (jak obiekt). "
Matt Klein,
Najbardziej zagmatwana część tego wszystkiego polega na tym, że w przypadku kowariancji lub kontrawariancji, jeśli zignorujesz kierunek (do wewnątrz lub na zewnątrz), i tak uzyskasz konwersję Bardziej szczegółowe do bardziej ogólnej! Mam na myśli: „możesz traktować tę zwróconą wartość jako typ bardziej ogólny (jak obiekt)” dla kowariancji i: „API oczekuje czegoś ogólnego (jak obiekt), możesz nadać mu coś bardziej szczegółowego (np. String)” dla kontrawariancji . Dla mnie to brzmi tak samo!
XMight
@AlexanderDerck: Nie jestem pewien, dlaczego wcześniej ci nie odpowiedziałem; Zgadzam się, że jest to niejasne i spróbuję to wyjaśnić.
Jon Skeet
16

Mam nadzieję, że mój post pomoże uzyskać niezależny od języka pogląd na ten temat.

Podczas naszych wewnętrznych szkoleń pracowałem ze wspaniałą książką „Smalltalk, Objects and Design (Chamond Liu)” i przeformułowałem następujące przykłady.

Co oznacza „konsekwencja”? Pomysł polega na zaprojektowaniu hierarchii typów bezpiecznych dla typów z typami wysoce zastępowalnymi. Kluczem do uzyskania tej spójności jest zgodność oparta na podtypach, jeśli pracujesz w języku z typami statycznymi. (Omówimy tutaj zasadę substytucji Liskova (LSP) na wysokim poziomie).

Praktyczne przykłady (pseudo kod / nieprawidłowy w C #):

  • Kowariancja: Załóżmy, że ptaki składające jaja „konsekwentnie” ze statycznym typowaniem: jeśli typ Ptak składa jajko, czy podtyp Ptaka nie byłby podtypem Jajka? Np. Typ Duck składa DuckEgg, wtedy jest podana konsystencja. Dlaczego jest to spójne? Ponieważ w takim wyrażeniu: Egg anEgg = aBird.Lay();referencja aBird mogłaby zostać prawnie zastąpiona przez instancję Bird lub Duck. Mówimy, że zwracany typ jest kowariantny w stosunku do typu, w którym zdefiniowano Lay (). Przesłonięcie podtypu może zwrócić bardziej wyspecjalizowany typ. => „Dostarczają więcej”.

  • Kontrawariancja: Załóżmy, że pianiści potrafią grać „konsekwentnie” przy użyciu statycznego pisania: jeśli pianista gra na pianinie, czy byłby w stanie grać na fortepianie? Czy nie wolałbyś, żeby Wirtuoz grał na fortepianie? (Ostrzegam; jest coś dziwnego!) To jest niespójne! Bo w takim ujęciu: aPiano.Play(aPianist);aPiano nie może być legalnie zastąpione przez Piano ani przez instancję GrandPiano! Na fortepianie może grać tylko wirtuoz, pianiści są zbyt ogólni! GrandPianos muszą być grywalne przez bardziej ogólne typy, wtedy gra jest spójna. Mówimy, że typ parametru jest sprzeczny z typem, w którym zdefiniowano Play (). Przesłonięcie podtypu może akceptować bardziej uogólniony typ. => „Wymagają mniej”.

Powrót do C #:
Ponieważ C # jest zasadniczo językiem z typowaniem statycznym, „lokalizacje” interfejsu typu, które powinny być współ- lub kontrawariantne (np. Parametry i typy zwracane), muszą być wyraźnie oznaczone, aby zagwarantować spójne użycie / rozwój tego typu , aby LSP działało dobrze. W językach z typami dynamicznymi spójność LSP zazwyczaj nie stanowi problemu, innymi słowy, można całkowicie pozbyć się współ- i kontrawariantnych „znaczników” w interfejsach i delegatach .Net, jeśli używałbyś tylko typu dynamic w swoich typach. - Ale to nie jest najlepsze rozwiązanie w C # (nie należy używać dynamiki w interfejsach publicznych).

Powrót do teorii:
Opisana zgodność (kowariantne typy zwracane / kontrawariantne typy parametrów) jest teoretycznym ideałem (obsługiwanym przez języki Emerald i POOL-1). Niektóre języki oop (np. Eiffel) zdecydowały się zastosować inny rodzaj spójności, zwł. także kowariantne typy parametrów, ponieważ lepiej opisuje rzeczywistość niż ideał teoretyczny. W językach z typami statycznymi pożądaną spójność trzeba często osiągnąć przez zastosowanie wzorców projektowych, takich jak „podwójne wysyłanie” i „odwiedzający”. Inne języki zapewniają tak zwane „wielokrotne wysyłanie” lub wiele metod (jest to w zasadzie wybieranie przeciążeń funkcji w czasie wykonywania , np. Z CLOS) lub uzyskują pożądany efekt za pomocą dynamicznego pisania.

Nico
źródło
Mówisz, że przesłonięcie podtypu A może zwrócić bardziej wyspecjalizowany typ . Ale to jest całkowicie nieprawdziwe. Jeśli Birddefiniuje public abstract BirdEgg Lay();, Duck : Bird MUSI zaimplementować, public override BirdEgg Lay(){}więc twoje twierdzenie, które BirdEgg anEgg = aBird.Lay();ma w ogóle jakąkolwiek rozbieżność, jest po prostu nieprawdziwe. Będąc przesłanką celu wyjaśnienia, cały punkt już minął. Czy zamiast tego można powiedzieć, że kowariancja istnieje w implementacji, w której DuckEgg jest niejawnie rzutowana na typ wyjścia / powrotu BirdEgg? Tak czy inaczej, proszę, usuń moje zamieszanie.
Suamere
1
Krótko mówiąc: masz rację! Przepraszam za zamieszanie. DuckEgg Lay()nie jest prawidłowym przesłonięciem dla Egg Lay() języka C # i to jest sedno. C # nie obsługuje kowariantnych typów zwracanych, ale Java i C ++ to robią. Raczej opisałem ideał teoretyczny przy użyciu składni podobnej do C #. W C # musisz pozwolić Birdowi i Kaczce zaimplementować wspólny interfejs, w którym Lay jest zdefiniowany tak, aby miał kowariantny typ powrotu (tj. Poza specyfikacją), a następnie sprawy pasują do siebie!
Nico
1
Analogicznie do komentarza Matta-Kleina na temat odpowiedzi @Jona-Skeeta, „do mojego przyszłego ja”: „Najlepszym wnioskiem dla mnie jest„ Dostarczają więcej ”(konkretnie) i„ Wymagają mniej ”(konkretnie). „Wymagaj mniej i dostarczaj więcej” to doskonały mnemonik! Jest to analogiczne do pracy, w której mam nadzieję wymagać mniej szczegółowych instrukcji (ogólnych próśb), a jednocześnie dostarczyć coś bardziej konkretnego (rzeczywisty produkt pracy). Tak czy inaczej, kolejność podtypów (LSP) jest nieprzerwana.
karfus
@karfus: Dziękuję, ale jak pamiętam sparafrazowałem ideę „Wymagaj mniej i dostarczaj więcej” z innego źródła. Może to była książka Liu, o której wspominam powyżej ... albo nawet rockowy wykład .NET. Przy okazji. w Javie ludzie zredukowali mnemonik do „PECS”, co bezpośrednio odnosi się do syntaktycznego sposobu deklarowania wariancji, PECS oznacza „producent extends, konsument super”.
Nico
5

Delegat konwertera pomaga mi zrozumieć różnicę.

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputreprezentuje kowariancję, w której metoda zwraca bardziej szczegółowy typ .

TInputreprezentuje kontrawariancję, w której metoda jest przekazywana do mniej określonego typu .

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();
Woggles
źródło
0

Wariancja Co i Contra to całkiem logiczne rzeczy. System typów języka zmusza nas do wspierania logiki prawdziwego życia. Łatwo to zrozumieć na przykładzie.

Kowariancja

Na przykład chcesz kupić kwiat, a masz w swoim mieście dwa sklepy z kwiatami: sklep z różami i sklep ze stokrotkami.

Jeśli zapytasz kogoś „gdzie jest kwiaciarnia?” a ktoś ci powie, gdzie jest sklep z różami, czy byłoby dobrze? Tak, ponieważ róża to kwiat, jeśli chcesz kupić kwiat, możesz kupić różę. To samo dotyczy sytuacji, gdy ktoś przesłał Ci adres sklepu ze stokrotkami.

To jest przykład kowariancji : możesz rzutować A<C>na A<B>, gdzie Cjest podklasą B, if Ageneruje wartości ogólne (zwraca jako wynik funkcji). Kowariancja dotyczy producentów, dlatego C # używa słowa kluczowego outdla kowariancji.

Rodzaje:

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();
    }
}

Pytanie brzmi „gdzie jest kwiaciarnia?”, Odpowiedź brzmi „tam sklep z różami”:

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

Sprzeczność

Na przykład chcesz podarować kwiat swojej dziewczynie, a twoja dziewczyna lubi wszystkie kwiaty. Czy możesz ją uznać za osobę, która kocha róże, czy osobę, która kocha stokrotki? Tak, ponieważ gdyby kochała jakikolwiek kwiat, pokochałaby zarówno różę, jak i stokrotkę.

To jest przykład kontrawariancji : możesz rzucać A<B>do A<C>, gdzie Cjest podklasa B, jeśli Azużywa wartość ogólną. Contravariance dotyczy konsumentów, dlatego C # używa słowa kluczowego indo kontrawariancji.

Rodzaje:

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!");
    }
}

Uważasz swoją dziewczynę, która kocha każdy kwiat, za kogoś, kto kocha róże i dajesz jej różę:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

Spinki do mankietów

VadzimV
źródło