Antworten:
Die Frage ist: "Was ist der Unterschied zwischen Kovarianz und Kontravarianz?"
Kovarianz und Kontravarianz sind Eigenschaften einer Zuordnungsfunktion, die ein Mitglied einer Menge mit einem anderen verknüpft . Insbesondere kann eine Abbildung in Bezug auf eine Beziehung auf dieser Menge kovariant oder kontravariant sein .
Betrachten Sie die folgenden zwei Teilmengen der Menge aller C # -Typen. Zuerst:
{ Animal,
Tiger,
Fruit,
Banana }.
Und zweitens diese klar verwandte Menge:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
Es gibt eine Zuordnungsoperation vom ersten zum zweiten Satz. Das heißt, für jedes T im ersten Satz ist der entsprechende Typ im zweiten Satz IEnumerable<T>
. Oder in Kurzform ist das Mapping T → IE<T>
. Beachten Sie, dass dies ein "dünner Pfeil" ist.
Bisher bei mir?
Betrachten wir nun eine Beziehung . Es gibt eine Zuweisungskompatibilitätsbeziehung zwischen Typpaaren im ersten Satz. Ein Wert vom Typ Tiger
kann einer Variablen vom Typ zugewiesen werden Animal
, daher werden diese Typen als "zuweisungskompatibel" bezeichnet. Schreiben wir in kürzerer Form "Ein Wert vom Typ X
kann einer Variablen vom Typ zugewiesen werden Y
":X ⇒ Y
. Beachten Sie, dass dies ein "fetter Pfeil" ist.
In unserer ersten Teilmenge sind hier alle Zuordnungskompatibilitätsbeziehungen aufgeführt:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
In C # 4, das die kovariante Zuweisungskompatibilität bestimmter Schnittstellen unterstützt, gibt es eine Zuweisungskompatibilitätsbeziehung zwischen Typpaaren im zweiten Satz:
IE<Tiger> ⇒ IE<Tiger>
IE<Tiger> ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit> ⇒ IE<Fruit>
Beachten Sie, dass die Zuordnung T → IE<T>
die Existenz und Richtung der Zuweisungskompatibilität beibehält . Das heißt, wenn X ⇒ Y
, dann ist es auch wahr, dassIE<X> ⇒ IE<Y>
.
Wenn wir zwei Dinge auf beiden Seiten eines fetten Pfeils haben, können wir beide Seiten durch etwas auf der rechten Seite eines entsprechenden dünnen Pfeils ersetzen.
Eine Abbildung, die diese Eigenschaft in Bezug auf eine bestimmte Beziehung aufweist, wird als "kovariante Abbildung" bezeichnet. Dies sollte sinnvoll sein: Eine Folge von Tigern kann verwendet werden, wenn eine Folge von Tieren benötigt wird, aber das Gegenteil ist nicht der Fall. Eine Sequenz von Tieren kann nicht unbedingt verwendet werden, wenn eine Sequenz von Tigern benötigt wird.
Das ist Kovarianz. Betrachten Sie nun diese Teilmenge der Menge aller Typen:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
Jetzt haben wir die Zuordnung vom ersten zum dritten Satz T → IC<T>
.
In 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>
Das heißt, die Zuordnung T → IC<T>
hat die Existenz bewahrt, aber die Richtung der Zuweisungskompatibilität umgekehrt . Das heißt, wenn X ⇒ Y
, dannIC<X> ⇐ IC<Y>
.
Eine Zuordnung , die eine Beziehung beibehält, aber umkehrt, wird als kontravariante Zuordnung bezeichnet.
Auch dies sollte eindeutig korrekt sein. Ein Gerät, das zwei Tiere vergleichen kann, kann auch zwei Tiger vergleichen, aber ein Gerät, das zwei Tiger vergleichen kann, kann nicht unbedingt zwei Tiere vergleichen.
So dass die Differenz zwischen Kovarianz und Kontra in C # 4. Kovarianz ist bewahrt die Richtung der Zuordenbarkeit. Kontravarianz kehrt es um.
IEnumerable<Tiger>
zu IEnumerable<Animal>
sicher? Weil es keine Möglichkeit gibt, eine Giraffe einzugebenIEnumerable<Animal>
. Warum können wir ein IComparable<Animal>
in konvertieren IComparable<Tiger>
? Weil es keine Möglichkeit gibt , eine Giraffe aus einem herauszunehmenIComparable<Animal>
. Sinn ergeben?
Es ist wahrscheinlich am einfachsten, Beispiele zu nennen - so erinnere ich mich sicherlich an sie.
Kovarianz
Canonical Beispiele: IEnumerable<out T>
,Func<out T>
Sie können von IEnumerable<string>
nach IEnumerable<object>
oder Func<string>
nach konvertieren Func<object>
. Werte kommen nur aus diesen Objekten heraus.
Dies funktioniert, da Sie string
diesen zurückgegebenen Wert als allgemeineren Typ (wie object
) behandeln können, wenn Sie nur Werte aus der API entfernen und etwas Bestimmtes (wie ) zurückgeben .
Kontravarianz
Canonical Beispiele: IComparer<in T>
,Action<in T>
Sie können von IComparer<object>
nach IComparer<string>
oder Action<object>
nach konvertieren Action<string>
. Werte gehen nur in diese Objekte.
Diesmal funktioniert es, denn wenn die API etwas Allgemeines (wie object
) erwartet, können Sie ihr etwas Spezifischeres (wie string
) geben.
Allgemeiner
Wenn Sie eine Schnittstelle IFoo<T>
haben, kann diese kovariant sein T
(dh deklarieren, als IFoo<out T>
ob sie T
nur an einer Ausgabeposition (z. B. einem Rückgabetyp) innerhalb der Schnittstelle verwendet wird. Sie kann in T
(dh IFoo<in T>
) kontravariant sein, wenn sie T
nur an einer Eingabeposition verwendet wird (z. zB ein Parametertyp).
Es wird möglicherweise verwirrend, weil "Ausgabeposition" nicht ganz so einfach ist, wie es sich anhört - ein Parameter vom Typ Action<T>
wird immer noch nur T
in einer Ausgabeposition verwendet - die Kontravarianz von Action<T>
dreht es um, wenn Sie sehen, was ich meine. Es ist insofern eine "Ausgabe", als die Werte von der Implementierung der Methode zum Code des Aufrufers übergehen können , genau wie es ein Rückgabewert kann. Normalerweise kommt so etwas zum Glück nicht auf :)
Action<T>
wird immer noch nur T
an einer Ausgabeposition verwendet" . Action<T>
Der Rückgabetyp ist ungültig. Wie kann er T
als Ausgabe verwendet werden? Oder ist es das, was es bedeutet, weil es nichts zurückgibt, was Sie sehen können, dass es niemals gegen die Regel verstoßen kann?
Ich hoffe, mein Beitrag hilft dabei, eine sprachunabhängige Sicht auf das Thema zu bekommen.
Für unsere internen Schulungen habe ich mit dem wunderbaren Buch "Smalltalk, Objekte und Design (Chamond Liu)" gearbeitet und die folgenden Beispiele umformuliert.
Was bedeutet "Konsistenz"? Die Idee ist, typsichere Typhierarchien mit stark ersetzbaren Typen zu entwerfen. Der Schlüssel zum Erreichen dieser Konsistenz ist die auf Subtypen basierende Konformität, wenn Sie in einer statisch typisierten Sprache arbeiten. (Wir werden das Liskov-Substitutionsprinzip (LSP) hier auf hoher Ebene diskutieren.)
Praktische Beispiele (Pseudocode / ungültig in C #):
Kovarianz: Nehmen wir an, Vögel, die Eier „konsistent“ mit statischer Typisierung legen: Wenn der Typ Vogel ein Ei legt, würde der Subtyp des Vogels dann nicht einen Subtyp des Eies legen? ZB legt der Typ Ente ein Entenei, dann ist die Konsistenz gegeben. Warum ist das so konsequent? Denn in einem solchen Ausdruck: Egg anEgg = aBird.Lay();
Die Referenz aBird könnte legal durch einen Vogel oder eine Enteninstanz ersetzt werden. Wir sagen, der Rückgabetyp ist kovariant zu dem Typ, in dem Lay () definiert ist. Die Überschreibung eines Subtyps kann einen spezielleren Typ zurückgeben. => "Sie liefern mehr."
Kontravarianz: Nehmen wir an, dass Pianisten mit statischer Typisierung „konsistent“ spielen können: Wenn eine Pianistin Klavier spielt, kann sie dann einen Flügel spielen? Würde ein Virtuose nicht lieber einen Flügel spielen? (Seien Sie gewarnt; es gibt eine Wendung!) Dies ist inkonsistent! Denn in einem solchen Ausdruck: aPiano.Play(aPianist);
aPiano konnte nicht legal durch ein Piano oder eine GrandPiano-Instanz ersetzt werden! Ein Flügel kann nur von einem Virtuosen gespielt werden, Pianisten sind zu allgemein! Flügel müssen von allgemeineren Typen spielbar sein, dann ist das Spiel konsistent. Wir sagen, dass der Parametertyp nicht mit dem Typ übereinstimmt, in dem Play () definiert ist. Die Überschreibung eines Subtyps akzeptiert möglicherweise einen allgemeineren Typ. => "Sie benötigen weniger."
Zurück zu C #:
Da C # im Grunde eine statisch typisierte Sprache ist, müssen die "Positionen" der Schnittstelle eines Typs, die co- oder kontravariant sein sollten (z. B. Parameter und Rückgabetypen), explizit markiert werden, um eine konsistente Verwendung / Entwicklung dieses Typs zu gewährleisten , damit der LSP gut funktioniert. In dynamisch typisierten Sprachen ist die LSP-Konsistenz normalerweise kein Problem. Mit anderen Worten, Sie könnten Co- und Contravarianten-Markups auf .Net-Schnittstellen und -Delegaten vollständig entfernen, wenn Sie nur die Typdynamik in Ihren Typen verwenden. - Dies ist jedoch nicht die beste Lösung in C # (Sie sollten Dynamic in öffentlichen Schnittstellen nicht verwenden).
Zurück zur Theorie:
Die beschriebene Konformität (kovariante Rückgabetypen / kontravariante Parametertypen) ist das theoretische Ideal (unterstützt durch die Sprachen Emerald und POOL-1). Einige oop-Sprachen (z. B. Eiffel) haben beschlossen, eine andere Art von Konsistenz anzuwenden, insbesondere auch kovariante Parametertypen, weil sie die Realität besser beschreiben als das theoretische Ideal. In statisch typisierten Sprachen muss die gewünschte Konsistenz häufig durch Anwendung von Entwurfsmustern wie „Doppelversand“ und „Besucher“ erreicht werden. Andere Sprachen bieten sogenannte "Multiple Dispatch" - oder Multi-Methoden (dies ist im Grunde die Auswahl von Funktionsüberladungen zur Laufzeit , z. B. mit CLOS) oder erzielen den gewünschten Effekt durch dynamische Typisierung.
Bird
definiert public abstract BirdEgg Lay();
, dann Duck : Bird
MUSS implementiert werden. public override BirdEgg Lay(){}
Ihre Behauptung, die BirdEgg anEgg = aBird.Lay();
überhaupt irgendeine Varianz aufweist, ist einfach falsch. Als Prämisse des Erklärungspunktes ist nun der gesamte Punkt weg. Würden Sie stattdessen sagen, dass die Kovarianz innerhalb der Implementierung existiert, in der ein DuckEgg implizit in den BirdEgg-Out / Return-Typ umgewandelt wird? Wie auch immer, bitte klären Sie meine Verwirrung.
DuckEgg Lay()
ist keine gültige Überschreibung für Egg Lay()
in C # , und das ist der springende Punkt. C # unterstützt keine kovarianten Rückgabetypen, Java jedoch ebenso wie C ++. Ich habe das theoretische Ideal eher mit einer C # -ähnlichen Syntax beschrieben. In C # müssen Sie Bird and Duck eine gemeinsame Schnittstelle implementieren lassen, in der Lay so definiert ist, dass sie einen kovarianten Rückgabetyp (dh den Out-Specification-Typ) aufweist. Dann passen die Dinge zusammen!
extends
, Consumer super
".
Der Konverterdelegierte hilft mir, den Unterschied zu verstehen.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
stellt die Kovarianz dar, bei der eine Methode einen spezifischeren Typ zurückgibt .
TInput
stellt eine Kontravarianz dar, bei der eine Methode einem weniger spezifischen Typ übergeben wird .
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- und Contra-Varianz sind ziemlich logische Dinge. Das Sprachtypsystem zwingt uns, die Logik des wirklichen Lebens zu unterstützen. Es ist leicht anhand eines Beispiels zu verstehen.
Zum Beispiel möchten Sie eine Blume kaufen und haben zwei Blumengeschäfte in Ihrer Stadt: Rosengeschäft und Gänseblümchengeschäft.
Wenn Sie jemanden fragen "Wo ist der Blumenladen?" und jemand sagt dir, wo ist Rosenladen, wäre es okay? Ja, denn Rose ist eine Blume. Wenn Sie eine Blume kaufen möchten, können Sie eine Rose kaufen. Gleiches gilt, wenn Ihnen jemand mit der Adresse des Gänseblümchenladens geantwortet hat.
Dies ist beispielsweise der Kovarianz : Sie dürfen auf Guss A<C>
zu A<B>
, wo C
es eine Unterklasse von B
, wenn A
generische Werte erzeugt (kehrt als Ergebnis aus der Funktion). Bei der Kovarianz geht es um Produzenten. Deshalb verwendet C # das Schlüsselwort out
für die Kovarianz.
Typen:
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();
}
}
Die Frage lautet "Wo ist der Blumenladen?", Die Antwort lautet "Rosenladen dort":
static FlowerShop<Flower> tellMeShopAddress() {
return new RoseShop();
}
Zum Beispiel möchten Sie Ihrer Freundin eine Blume schenken und Ihre Freundin mag Blumen. Können Sie sie als eine Person betrachten, die Rosen liebt, oder als eine Person, die Gänseblümchen liebt? Ja, denn wenn sie eine Blume liebt, würde sie sowohl Rose als auch Gänseblümchen lieben.
Dies ist ein Beispiel für die Kontra : Sie Guss erlaubt sind A<B>
zu A<C>
, wo C
ist Unterklasse von B
, wenn A
verbraucht generischen Wert. Bei Kontravarianz geht es um Verbraucher. Deshalb verwendet C # das Schlüsselwort in
für Kontravarianz.
Typen:
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!");
}
}
Sie betrachten Ihre Freundin, die jede Blume liebt, als jemanden, der Rosen liebt, und geben ihr eine Rose:
PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());