Stufe 3Objekte und Typen · Lektion 16 von 34
Generics: Typparameter und Varianz
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
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 // 2A 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:
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) // 0Die 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:
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:
| Schreibweise | Name | Bedeutung für Hund <: Tier |
|---|---|---|
Kiste[A] | invariant | Kiste[Hund] und Kiste[Tier] haben nichts miteinander zu tun |
Kiste[+A] | kovariant | Kiste[Hund] ist ein Untertyp von Kiste[Tier] |
Kiste[-A] | kontravariant | Kiste[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:
val hunde: List[Hund] = List(Hund("Rex"))
val tiere: List[Tier] = hunde // erlaubt, weil List kovariant istDas 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:
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 kontravariantFunktionstypen 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:
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 TierGenau 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:
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.