Unterschied zwischen Kovarianz und Kontravarianz

Antworten:

265

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 Tigerkann einer Variablen vom Typ zugewiesen werden Animal, daher werden diese Typen als "zuweisungskompatibel" bezeichnet. Schreiben wir in kürzerer Form "Ein Wert vom Typ Xkann 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.

Eric Lippert
quelle
4
Für jemanden wie mich wäre es besser gewesen, Beispiele hinzuzufügen, die zeigen, was NICHT kovariant und was NICHT kontravariant ist und was NICHT beides ist.
Bjan
2
@ Bargitta: Es ist sehr ähnlich. Der Unterschied besteht darin, dass C # eine bestimmte Site-Varianz verwendet und Java die Call-Site-Varianz verwendet . Die Art und Weise, wie die Dinge variieren, ist die gleiche, aber wo der Entwickler sagt "Ich brauche das, um eine Variante zu sein", ist anders. Das Feature in beiden Sprachen wurde übrigens teilweise von derselben Person entworfen!
Eric Lippert
2
@AshishNegi: Lesen Sie den Pfeil als "darf verwendet werden als". "Eine Sache, die Tiere vergleichen kann, kann als eine Sache verwendet werden, die Tiger vergleichen kann". Jetzt Sinn machen?
Eric Lippert
1
@ AshishNegi: Nein, das ist nicht richtig. IEnumerable ist kovariant, da T nur in den Rückgaben der Methoden von IEnumerable erscheint. Und IComparable ist kontravariant, weil T nur als formale Parameter der Methoden von IComparable erscheint .
Eric Lippert
2
@AshishNegi: Sie möchten über die logischen Gründe nachdenken, die diesen Beziehungen zugrunde liegen. Warum können wir konvertieren 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?
Eric Lippert
111

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 stringdiesen 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 Tnur 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 Tnur 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 Tin 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 :)

Jon Skeet
quelle
1
Für jemanden wie mich wäre es besser gewesen, Beispiele hinzuzufügen, die zeigen, was NICHT kovariant und was NICHT kontravariant ist und was NICHT beides ist.
Bjan
1
@ Jon Skeet Schönes Beispiel, ich verstehe nur nicht "ein Parameter vom Typ Action<T>wird immer noch nur Tan einer Ausgabeposition verwendet" . Action<T>Der Rückgabetyp ist ungültig. Wie kann er Tals 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?
Alexander Derck
2
Um meine Zukunft selbst, zurück zu dieser ausgezeichneten Antwort , die kommt wieder den Unterschied wieder zu erlernen, das ist die Linie , die Sie wollen: „[Kovarianzstrukturen] funktioniert , weil , wenn Sie nur Werte aus dem API nehmen, und es geht etwas zurück spezifisch (wie Zeichenfolge) können Sie diesen zurückgegebenen Wert als allgemeineren Typ (wie Objekt) behandeln. "
Matt Klein
Der verwirrendste Teil von alledem ist, dass entweder für Kovarianz oder Kontravarianz, wenn Sie die Richtung (rein oder raus) ignorieren, Sie sowieso eine spezifischere zu einer allgemeineren Konvertierung erhalten! Ich meine: "Sie können diesen zurückgegebenen Wert als allgemeineren Typ (wie Objekt) behandeln" für Kovarianz und: "API erwartet etwas Allgemeines (wie Objekt), Sie können ihm etwas Spezifischeres (wie Zeichenfolge) geben" für Kontravarianz . Für mich klingen diese ähnlich!
XMight
@AlexanderDerck: Ich bin mir nicht sicher, warum ich dir vorher nicht geantwortet habe. Ich bin damit einverstanden, dass es unklar ist, und werde versuchen, es zu klären.
Jon Skeet
16

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.

Nico
quelle
Sie sagen, dass die Überschreibung eines Subtyps möglicherweise einen spezielleren Typ zurückgibt . Das ist aber völlig falsch. Wenn Birddefiniert 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.
Suamere
1
Um es kurz zu machen: Sie haben Recht! Entschuldigung für die 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!
Nico
1
Als Analogie zu Matt-Kleins Kommentar zu @ Jon-Skeets Antwort "Auf mein zukünftiges Selbst": Der beste Weg für mich hier ist "Sie liefern mehr" (spezifisch) und "Sie benötigen weniger" (spezifisch). "Benötigen Sie weniger und liefern Sie mehr" ist eine ausgezeichnete Mnemonik! Es ist analog zu einem Job, bei dem ich hoffe, weniger spezifische Anweisungen (allgemeine Anforderungen) zu benötigen und dennoch etwas Spezifischeres (ein tatsächliches Arbeitsprodukt) zu liefern. In beiden Fällen ist die Reihenfolge der Subtypen (LSP) nicht unterbrochen.
Karfus
@karfus: Danke, aber wie ich mich erinnere, habe ich die Idee "Weniger verlangen und mehr liefern" aus einer anderen Quelle umschrieben. Könnte es sein, dass es Lius Buch war, auf das ich mich oben beziehe ... oder sogar ein .NET Rock-Vortrag. Übrigens. In Java wurde die Mnemonik auf "PECS" reduziert, was in direktem Zusammenhang mit der syntaktischen Methode zum Deklarieren von Abweichungen steht. PECS steht für "Producer extends, Consumer super".
Nico
5

Der Konverterdelegierte hilft mir, den Unterschied zu verstehen.

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

TOutputstellt die Kovarianz dar, bei der eine Methode einen spezifischeren Typ zurückgibt .

TInputstellt 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();
Woggles
quelle
0

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.

Kovarianz

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 Ces eine Unterklasse von B, wenn Agenerische Werte erzeugt (kehrt als Ergebnis aus der Funktion). Bei der Kovarianz geht es um Produzenten. Deshalb verwendet C # das Schlüsselwort outfü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();
}

Kontravarianz

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 Cist Unterklasse von B, wenn Averbraucht generischen Wert. Bei Kontravarianz geht es um Verbraucher. Deshalb verwendet C # das Schlüsselwort infü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());

Links

VadzimV
quelle