Mam problem ze zrozumieniem różnicy między kowariancją a kontrawariancją.
źródło
Mam problem ze zrozumieniem różnicy między kowariancją a kontrawariancją.
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 Tiger
może być przypisana do zmiennej typu Animal
, więc te typy są określane jako „zgodne z przypisaniem”. Write Chodźmy „wartość typu X
moż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 ⇒ Y
wtedy 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 .
IEnumerable<Tiger>
sięIEnumerable<Animal>
bezpiecznie? Ponieważ nie ma sposobu, aby wprowadzić żyrafę doIEnumerable<Animal>
. Dlaczego możemy przekonwertowaćIComparable<Animal>
naIComparable<Tiger>
? Ponieważ nie ma sposobu, aby wyjąć żyrafę z plikuIComparable<Animal>
. Ma sens?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>
naIEnumerable<object>
lubFunc<string>
naFunc<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ładobject
).Sprzeczność
Przykłady: kanoniczne
IComparer<in T>
,Action<in T>
Możesz konwertować z
IComparer<object>
doIComparer<string>
lubAction<object>
doAction<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 (npstring
.).Bardziej ogólnie
Jeśli masz interfejs
IFoo<T>
, może być kowariantny wT
(tj. Zadeklarować go tak,IFoo<out T>
jakbyT
był używany tylko w pozycji wyjściowej (np. Typ powrotu) w interfejsie. Może być kontrawariantny wT
(tj.IFoo<in T>
), JeśliT
jest 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 tylkoT
w pozycji wyjściowej - kontrawariancjaAction<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ą :)źródło
Action<T>
jest nadal używany tylkoT
w pozycji wyjściowej” .Action<T>
zwracany typ jest void, w jaki sposób można go używaćT
jako 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?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.
źródło
Bird
definiujepublic abstract BirdEgg Lay();
,Duck : Bird
MUSI zaimplementować,public override BirdEgg Lay(){}
więc twoje twierdzenie, któreBirdEgg 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.DuckEgg Lay()
nie jest prawidłowym przesłonięciem dlaEgg 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!extends
, konsumentsuper
”.Delegat konwertera pomaga mi zrozumieć różnicę.
TOutput
reprezentuje kowariancję, w której metoda zwraca bardziej szczegółowy typ .TInput
reprezentuje kontrawariancję, w której metoda jest przekazywana do mniej określonego typu .źródło
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>
naA<B>
, gdzieC
jest podklasąB
, ifA
generuje wartości ogólne (zwraca jako wynik funkcji). Kowariancja dotyczy producentów, dlatego C # używa słowa kluczowegoout
dla kowariancji.Rodzaje:
Pytanie brzmi „gdzie jest kwiaciarnia?”, Odpowiedź brzmi „tam sklep z różami”:
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>
doA<C>
, gdzieC
jest podklasaB
, jeśliA
zużywa wartość ogólną. Contravariance dotyczy konsumentów, dlatego C # używa słowa kluczowegoin
do kontrawariancji.Rodzaje:
Uważasz swoją dziewczynę, która kocha każdy kwiat, za kogoś, kto kocha róże i dajesz jej różę:
Spinki do mankietów
źródło