Odpowiedzi:
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ę 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?
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 T
był używany tylko w pozycji wyjściowej (np. Typ powrotu) w interfejsie. Może być kontrawariantny w T
(tj. IFoo<in T>
), Jeśli T
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 tylko T
w 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ą :)
Action<T>
jest nadal używany tylko T
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.
Bird
definiuje 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.
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!
extends
, konsument super
”.
Delegat konwertera pomaga mi zrozumieć różnicę.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
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 .
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();
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.
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 C
jest podklasą B
, if A
generuje wartości ogólne (zwraca jako wynik funkcji). Kowariancja dotyczy producentów, dlatego C # używa słowa kluczowego out
dla 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();
}
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 C
jest podklasa B
, jeśli A
zużywa wartość ogólną. Contravariance dotyczy konsumentów, dlatego C # używa słowa kluczowego in
do 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());