Denken Sie immer daran, dass, wenn Third
erweitert Second
ist, wenn immer ein Second
gewünscht wird, ein Third
bereitgestellt werden kann. Dies wird als Subtyp polymorhpism bezeichnet.
In diesem Sinne ist es natürlich, dass second.printInvariant(new Third)
kompiliert. Sie haben eine Third
zur Verfügung gestellt, die eine Unterart von Second
ist, so dass es auscheckt. Es ist, als würde man einem Apfel eine Methode geben, die eine Frucht nimmt. Diese
bedeutet, dass Ihre Methode
def printCovariant[A<:B](a: A): Unit = println(a)
kann geschrieben werden:
def printCovariant(a: B): Unit = println(a)
, ohne Informationen zu verlieren. Aufgrund des Subtyp-Polymorphismus akzeptiert der zweite B und alle seine Unterklassen, was derselbe wie der erste ist.
Das gleiche gilt für Ihren zweiten Fehlerfall - es ist ein weiterer Fall von Subtyp Polymorphismus. Sie können die neue Third übergeben, weil Third tatsächlich eine Sekunde ist (beachten Sie, dass ich die "is-a" Beziehung zwischen Unterklasse und Oberklasse aus der objektorientierten Notation verwende).
Falls Sie sich fragen, warum haben wir auch obere Schranken müssen, beachten Sie dieses Beispiel aus (ist genug Polymorphismus nicht Subtyp?):
def foo1[A <: AnyRef](xs: A) = xs
def foo2(xs: AnyRef) = xs
val res1 = foo1("something") // res1 is a String
val res2 = foo2("something") // res2 is an Anyref
Jetzt machen wir den Unterschied bemerken. Obwohl der Subtyp-Polymorphismus es uns erlaubt, in beiden Fällen einen String zu übergeben, kann nur die Methode foo1
den Typ des Arguments (in unserem Fall ein String) referenzieren. Methode foo2
nimmt glücklich eine Zeichenfolge, aber wird nicht wirklich wissen, dass es eine Zeichenfolge ist. Obere Grenzen können also nützlich sein, wenn Sie den Typ beibehalten möchten (in Ihrem Fall drucken Sie einfach den Wert aus, so dass Sie sich nicht wirklich um den Typ kümmern - alle Typen haben eine toString-Methode).
EDIT:
(zusätzliche Details, können Sie bereits wissen, aber ich werde es auf Vollständigkeit stellen)
Es gibt mehrere Verwendungszwecke obere Schranken dann, was ich hier beschrieben, aber bei der Parametrierung Eine Methode ist das häufigste Szenario. Wenn Sie eine Klasse parametrisieren, können Sie obere Grenzen zur Beschreibung der Kovarianz und unteren Grenzen zur Beschreibung der Kontravarianz verwenden. Zum Beispiel
class SomeClass[U] {
def someMethod(foo: Foo[_ <: U]) = ???
}
sagt, dass Parameter foo
der Methode someMethod
in seiner Art kovariant ist. Wie ist das? Nun, normalerweise (dh ohne Varianz zu verändern) würde es uns der Subtyp-Polymorphismus nicht erlauben, einen Foo
Parameter zu übergeben, der mit einem Subtyp seines Typparameters parametrisiert ist. Wenn T <: U
, bedeutet dies nicht, dass Foo[T] <: Foo[U]
. Wir sagen, dass Foo
in seinem Typ invariant ist. Aber wir haben nur die Methode optimiert, um Foo
mit U
oder einer seiner Subtypen akzeptiert zu akzeptieren.Nun, das ist effektiv Kovarianz. Solange also someMethod
betroffen ist - wenn ein Typ T
ein Untertyp von U
ist, dann ist Foo[T]
ein Untertyp von Foo[U]
. Großartig, wir haben Kovarianz erreicht. Aber beachte, dass ich sagte "solange someMethod
betroffen ist". Foo
ist in dieser Methode in seinem Typ kovariant, aber in anderen kann es invariant oder kontravariant sein.
Diese Art der Varianz Erklärung Nutzung Ort Varianz genannt, weil wir die Varianz einer Art an der Stelle seiner Verwendung (hier es verwendet als Methode Parametertyp someMethod
) deklarieren. Dies ist die einzige Art von Varianzdeklaration in Java. Bei der Verwendung von Site-Varianz haben Sie auf das Get-Put-Prinzip achten (google it). Im Grunde besagt dieses Prinzip, dass wir nur Zeug aus kovarianten Klassen bekommen können (wir können nicht setzen) und umgekehrt für kontravariante Klassen (wir können setzen, aber nicht bekommen). In unserem Fall können wir es wie folgt zeigen:
class Foo[T] { def put(t: T): Unit = println("I put some T") }
def someMethod(foo: Foo[_ <: String]) = foo.put("asd") // won't compile
def someMethod2(foo: Foo[_ >: String]) = foo.put("asd")
Allgemeiner können wir nur covariant Typen als Rückgabetypen und kontra-Typen als Parametertypen verwenden.
Jetzt ist use-site-Deklaration schön, aber in Scala ist es viel häufiger, Deklaration-Site- Varianz zu nutzen (etwas, das Java nicht hat). Dies bedeutet, dass wir die Varianz des generischen Typs Foo
zum Zeitpunkt der Definition von Foo
beschreiben würden. Wir würden einfach class Foo[+T]
sagen. Jetzt müssen wir beim Schreiben von Methoden, die mit Foo
arbeiten, keine Grenzen verwenden; wir haben Foo
proklamiert, um in seinem Typ, in jedem Anwendungsfall und jedem Szenario dauerhaft kovariant zu sein.
Für weitere Details über die Varianz in Scala zögern Sie nicht meine blog post zu diesem Thema.