Im Allgemeinen ist ein kovarianter Typparameter einer, der während der Subtypisierung der Klasse nach unten variieren darf (alternativ mit der Subtypisierung variieren, daher das Präfix "co-"). Konkreter:
trait List[+A]
List[Int]
ist ein Subtyp von List[AnyVal]
weil Int
ist ein Subtyp von AnyVal
. Dies bedeutet, dass Sie eine Instanz angeben können, in der List[Int]
ein Wert vom Typ List[AnyVal]
erwartet wird. Dies ist wirklich eine sehr intuitive Art und Weise, wie Generika arbeiten, aber es stellt sich heraus, dass es nicht gesund ist (das Typensystem bricht), wenn es in Gegenwart veränderlicher Daten verwendet wird. Aus diesem Grund sind Generika in Java unveränderlich. Kurzes Beispiel für Unklarheiten bei der Verwendung von Java-Arrays (die fälschlicherweise kovariant sind):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Wir haben gerade String
einem Array vom Typ einen Wert vom Typ zugewiesen Integer[]
. Aus Gründen, die offensichtlich sein sollten, sind dies schlechte Nachrichten. Das Java-Typsystem ermöglicht dies tatsächlich zur Kompilierungszeit. Die JVM wird ArrayStoreException
zur Laufzeit "hilfreich" einen werfen . Das Typensystem von Scala verhindert dieses Problem, da der Typparameter für die Array
Klasse unveränderlich ist (Deklaration ist [A]
eher als [+A]
).
Beachten Sie, dass es eine andere Art von Varianz gibt, die als Kontravarianz bekannt ist . Dies ist sehr wichtig, da es erklärt, warum Kovarianz einige Probleme verursachen kann. Kontravarianz ist buchstäblich das Gegenteil von Kovarianz: Die Parameter variieren mit der Subtypisierung nach oben . Es ist teilweise viel seltener, weil es so kontraintuitiv ist, obwohl es eine sehr wichtige Anwendung hat: Funktionen.
trait Function1[-P, +R] {
def apply(p: P): R
}
Beachten Sie die Varianzanmerkung " - " für den P
Typparameter. Diese Erklärung als Ganzes bedeutet, dass sie Function1
kontravariant P
und kovariant ist R
. Somit können wir die folgenden Axiome ableiten:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Beachten Sie, dass T1'
dies ein Subtyp (oder derselbe Typ) von sein muss T1
, während es für T2
und das Gegenteil ist T2'
. Auf Englisch kann dies wie folgt gelesen werden:
Eine Funktion A ist ein Subtyp einer anderen Funktion B, wenn der Parametertyp von A ein Supertyp des Parametertyps von B ist, während der Rückgabetyp von A ein Subtyp des Rückgabetyps von B ist .
Der Grund für diese Regel bleibt dem Leser als Übung überlassen (Hinweis: Denken Sie an verschiedene Fälle, wenn Funktionen subtypisiert sind, wie in meinem Array-Beispiel von oben).
Mit Ihrem neu gewonnenen Wissen über Co- und Kontravarianz sollten Sie erkennen können, warum das folgende Beispiel nicht kompiliert wird:
trait List[+A] {
def cons(hd: A): List[A]
}
Das Problem ist, dass A
es kovariant ist, während die cons
Funktion erwartet, dass ihr Typparameter invariant ist . Somit A
ändert sich die falsche Richtung. Interessanterweise könnten wir dieses Problem lösen, indem wir List
Contravariant in setzen A
, aber dann wäre der Rückgabetyp List[A]
ungültig, da die cons
Funktion erwartet, dass sein Rückgabetyp kovariant ist .
Unsere einzigen beiden Möglichkeiten sind: a) A
Invariante zu machen , die netten, intuitiven Subtypisierungseigenschaften der Kovarianz zu verlieren, oder b) der cons
Methode, die A
als Untergrenze definiert , einen lokalen Typparameter hinzuzufügen :
def cons[B >: A](v: B): List[B]
Dies ist jetzt gültig. Sie können sich vorstellen , dass A
nach unten variiert, aber in B
der Lage , sich nach oben zu variieren bezüglich A
da A
ist die Untergrenze. Mit dieser Methodendeklaration können wir A
kovariant sein und alles funktioniert.
Beachten Sie, dass dieser Trick nur funktioniert, wenn wir eine Instanz zurückgeben, List
die auf den weniger spezifischen Typ spezialisiert ist B
. Wenn Sie versuchen, List
veränderlich zu machen , brechen die Dinge zusammen, da Sie am Ende versuchen B
, einer Variablen vom Typ Werte vom Typ zuzuweisen A
, die vom Compiler nicht zugelassen werden. Wenn Sie veränderlich sind, benötigen Sie einen Mutator, für den ein Methodenparameter eines bestimmten Typs erforderlich ist, der (zusammen mit dem Accessor) eine Invarianz impliziert. Die Kovarianz arbeitet mit unveränderlichen Daten, da die einzig mögliche Operation ein Accessor ist, dem ein kovarianter Rückgabetyp zugewiesen werden kann.
var
es einstellbar ist, währendval
es nicht ist. Es ist der gleiche Grund, warum Scalas unveränderliche Sammlungen kovariant sind, die veränderlichen jedoch nicht.