scalatutorial.de

Stufe 3Objekte und Typen · Lektion 16 von 34

Generics: Typparameter und Varianz

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 4/6 dieser Stufe

Generics erlauben Klassen und Methoden, die mit beliebigen Typen funktionieren und trotzdem typsicher bleiben. Scala schreibt Typparameter in eckige Klammern und bietet mit Varianz und Typgrenzen feine Werkzeuge, um ihr Verhalten festzulegen.

Generische Klassen

 Scala 3
class Kiste[A](inhalt: A):
  def auspacken: A = inhalt
  def umpacken[B](f: A => B): Kiste[B] = Kiste(f(inhalt))

val k1 = Kiste(42)                 // Kiste[Int]
val k2 = k1.umpacken(_.toString)   // Kiste[String]
k2.auspacken.length                // 2

A ist ein Typparameter. Er steht in eckigen Klammern – in Scala sind spitze Klammern für Vergleiche reserviert. Beim Erzeugen leitet der Compiler den konkreten Typ meist selbst ab; Sie können ihn aber angeben: Kiste[Double](1).

Ohne Generics müssten Sie inhalt: Any schreiben und beim Auspacken umwandeln. Mit Generics weiß der Compiler an jeder Stelle, welcher Typ herauskommt.

Generische Methoden

Auch einzelne Methoden können Typparameter haben:

 Scala 3
def erstesOder[A](xs: List[A], ersatz: A): A =
  if xs.isEmpty then ersatz else xs.head

erstesOder(List("a", "b"), "leer")   // "a"
erstesOder(Nil, 0)                   // 0

Die gesamte Collections-Bibliothek ist so aufgebaut: List[A], Map[K, V], Option[A].

Obere Typgrenzen

Manchmal soll ein Typparameter nicht beliebig sein, sondern bestimmte Fähigkeiten haben. Eine obere Grenze A <: T verlangt, dass A ein Untertyp von T ist:

 Scala 3
trait Tier:
  def name: String

case class Hund(name: String) extends Tier
case class Katze(name: String) extends Tier

def namen[A <: Tier](tiere: List[A]): List[String] = tiere.map(_.name)

namen(List(Hund("Rex"), Hund("Bello")))

Varianz: ist eine Liste von Hunden eine Liste von Tieren?

Die Frage klingt harmlos, ist aber zentral. Scala lässt sie Sie für jeden Typparameter selbst beantworten:

SchreibweiseNameBedeutung für Hund <: Tier
Kiste[A]invariantKiste[Hund] und Kiste[Tier] haben nichts miteinander zu tun
Kiste[+A]kovariantKiste[Hund] ist ein Untertyp von Kiste[Tier]
Kiste[-A]kontravariantKiste[Tier] ist ein Untertyp von Kiste[Hund]

Kovarianz passt zu Behältern, aus denen man nur liest. Scalas List[+A] ist kovariant, deshalb dürfen Sie eine List[Hund] überall übergeben, wo eine List[Tier] erwartet wird:

 Scala 3
val hunde: List[Hund] = List(Hund("Rex"))
val tiere: List[Tier] = hunde   // erlaubt, weil List kovariant ist

Das geht gut, weil List unveränderlich ist. Bei einem veränderlichen Array[A] wäre es gefährlich – man könnte eine Katze in ein Hunde-Array legen. Deshalb ist Array in Scala invariant (in Java übrigens nicht, dort droht zur Laufzeit eine ArrayStoreException).

Kontravarianz passt zu Dingen, die Werte nur entgegennehmen – zum Beispiel Funktionsparameter. Ein Tierarzt für alle Tiere kann auch einen Hund behandeln:

 Scala 3
trait Behandler[-A]:
  def behandle(a: A): String

val tierarzt: Behandler[Tier] = t => s"${t.name} ist untersucht"
val hundearzt: Behandler[Hund] = tierarzt   // erlaubt, weil kontravariant

Funktionstypen kombinieren beides: A => B ist kontravariant im Parameter und kovariant im Ergebnis.

Untere Typgrenzen

Kovariante Klassen stoßen auf ein Problem, sobald eine Methode ein A entgegennimmt. Die Lösung ist eine untere Grenze B >: A:

 Scala 3
enum Stapel[+A]:
  case Leer
  case Oben(kopf: A, rest: Stapel[A])

  def push[B >: A](b: B): Stapel[B] = Oben(b, this)

val s = Stapel.Leer.push(Hund("Rex")).push(Katze("Mia"))
// Typ: ein Stapel des gemeinsamen Obertyps von Hund und Katze – im Kern Tier

Genau so ist :: bei List definiert: Legen Sie eine Katze auf eine Hundeliste, entsteht eine Liste des gemeinsamen Obertyps.

Context Bounds

Eine Schreibweise A: Ordering verlangt, dass es für A eine passende Typklassen-Instanz gibt:

 Scala 3
def groesstes[A: Ordering](xs: List[A]): A = xs.max

groesstes(List(3, 9, 2))            // 9
groesstes(List("Birne", "Apfel"))   // "Birne"

Wie diese Instanzen gefunden werden, erklärt die Lektion über given, using und implizite Konvertierung.

Passt dazu