Advanced Scala – Varianz - JAXenter
Teil 6 - Oder: “Ist eine Kiste voller Äpfel auch eine Kiste voller Obst?”

Advanced Scala – Varianz

Heiko Seeberger

Ist eine Kiste voller Äpfel auch eine Kiste voller Obst? Der gesunde Menschenverstand würde vermutlich mit „Aber sicher!“ antworten, schließlich zählen Äpfel zum Obst. Aber in der Objektorientierung bzw. beim Vererbungs-Polymorphismus müssen wir ein bisschen vorsichtiger sein. Warum?

Nun, solange wir nur „Dinge“ aus der Apfel-Kiste heraus nehmen, ist die Welt noch in Ordnung, selbst wenn wir sie als Obst-Kiste betrachten. Denn wenn die Obst-Kiste in Wahrheit eine Apfel-Kiste ist, dann kommen natürlich nur Äpfel heraus, die wir getrost als Obst betrachten können.

Aber nun drehen wir den Spieß einmal um und packen „Dinge“ zur Kiste hinzu. Wenn wir nun wieder unsere Apfel-Kiste als Obst-Kiste betrachten, dann dürften wir beliebiges Obst hineingeben, also auch Bananen, Pfirsiche etc. Und dann wäre die Apfel-Kiste ja keine Apfel-Kiste mehr. Wer denkt, dass das nicht so schlimm wäre, der denke bitte an einen Transportkäfig für Hunde – und wie dort ein Elefant hineinpassen soll.

Etwas formaler dreht sich diese Problematik um die Frage nach der sogenannten Varianz: Besteht aufgrund einer Vererbungsbeziehung zwischen Objekten ebenfalls eine Vererbungsbeziehung zwischen generischen Objekten, die mit den ersten parametrisiert sind? Falls nicht, so nennt man das invariant. Falls schon, so gilt es noch, die Richtungen der Vererbungsbeziehungen zu betrachten. Wenn diese gleichgerichtet sind, nennt man das kovariant, andernfalls kontravariant. Letzteres mag zunächst komisch anmuten, es gibt aber durchaus plausible Beispiele, wie wir später sehen werden.

Obiges Obst-Beispiel könnten wir in Scala folgendermaßen implementieren:

class Fruit
class Apple extends Fruit
class Box[A <: fruit="">

Wie verhält es sich hier mit der Varianz? Probieren wir es einfach aus:

scala> val fruitBox: Box[Fruit] = new Box[Apple]
:8: error: type mismatch;
...

scala> val appleBox: Box[Apple] = new Box[Fruit]
:8: error: type mismatch;
...

Das Beispiel zeigt, dass generische (parametrisierte) Typen in Scala grundsätzlich invariant sind. Wie wir anhand der Obst-Kiste gesehen haben, macht das im Allgemeinen durchaus Sinn.

Kovarianz

Aber wie wir ebenfalls gesehen haben, können wir dem Obst-Beispiel auch einen Spezialfall abgewinnen: Wenn wir uns dazu verpflichten, dass wir nichts mehr in die Kiste hinein geben, sondern nur heraus nehmen, dann wird unsere Kiste kovariant. In Scala können wir diese Verpflichtung zur Kovarianz durch Voranstellen eines Plus-Zeichens „+“ vor den Typparameter deklarieren:

class Box[+A <: fruit="">

Und siehe da, auf einmal ist auch für den Compiler eine Kiste mit Äpfeln eine Obst-Kiste:

scala> val fruitBox: Box[Fruit] = new Box[Apple]
fruitBox: Box[Fruit] = Box@6a754384

Aber was bedeutet das für unser Problem? Welche Auswirkung hat das Plus-Zeichen auf den Scala-Code? Wie wird sichergestellt, dass wir nichts mehr in die Kiste hinein geben können? Um das zu verstehen, müssen wir unser Beispiel ein bisschen erweitern. Zunächst fügen wir zur Box ein unveränderliches Feld hinzu:

class Box[+A <: fruit="" fruit:="" a="">

Nun können wir eine Kiste mit einem Apfel erzeugen und einem Wert vom Typ Obst-Kiste zuweisen:

scala> val fruitBox: Box[Fruit] = new Box(new Apple)
fruitBox: Box[Fruit] = Box@3c7a279c

scala> fruitBox.fruit                               
res7: Fruit = Apple@154174f9

Wir können natürlich von der Obst-Kiste den Inhalt abfragen. Aber wichtig ist, dass wir diesen nicht mehr verändern können. Damit das überhaupt möglich wäre, bräuchten wir eine Methode, die einen Parameter vom Typ A hat. Oder einfach ein veränderliches Feld statt eines unveränderlichen:

scala> class Box[+A <: fruit="" fruit:="" a="">:6: error: covariant type A occurs in contravariant position in type A of parameter of setter fruit_=
...

Wie wir sehen, lässt der Compiler so etwas aber gar nicht erst zu: Wenn wir einen generischen Typen als kovariant in einem seiner Typparameter deklarieren, dann darf dieser Typ nicht mehr an sogenannten kontravarianten Positionen auftauchen. Letztere sind schlicht Parameter von Methoden oder schreibbare Felder.

Geschrieben von
Heiko Seeberger
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.