Stufe 3Objekte und Typen · Lektion 14 von 34
Traits
Traits sind Scalas Antwort auf Interfaces – nur mächtiger: Sie dürfen Felder und fertige Methoden enthalten, und eine Klasse kann beliebig viele davon einmischen. So lassen sich Fähigkeiten baukastenartig kombinieren.
Ein Trait definieren
trait Begruessung:
def name: String // abstrakt
def gruss: String = s"Hallo, ich bin $name" // konkret
class Roboter(val name: String) extends Begruessung
Roboter("R2").gruss // "Hallo, ich bin R2"Ein Trait kann abstrakte Mitglieder haben, die die erbende Klasse liefern muss (hier name), und konkrete, die sie geschenkt bekommt (hier gruss). Eine Instanz eines Traits allein lässt sich nicht erzeugen – dafür braucht es eine Klasse oder ein object.
Mehrere Traits mischen
Eine Klasse erbt von höchstens einer Klasse, kann aber beliebig viele Traits einbinden. In Scala 3 werden sie mit Komma aufgezählt (in Scala 2 mit with, was weiterhin geht):
trait Tier:
def laut: String = "…"
trait Schwimmt:
def schwimmen() = println("platsch")
trait Fliegt:
def fliegen() = println("flatter")
trait Quakt extends Tier:
override def laut = "Quak"
class Ente extends Tier, Schwimmt, Fliegt, Quakt
val e = Ente()
e.schwimmen()
e.fliegen()
e.laut // "Quak"Diese Art, Fähigkeiten zusammenzustecken, nennt man Mixin-Komposition.
Linearisierung: wer gewinnt?
Was passiert, wenn mehrere Traits dieselbe Methode überschreiben? Scala löst das über eine feste Reihenfolge, die Linearisierung: Grob gesagt gewinnt der Trait, der am weitesten rechts steht. Ruft er super auf, geht es beim nächsten Trait links weiter.
Dadurch lassen sich Traits stapeln, jeder fügt seine Schicht hinzu:
trait Ausgabe:
def schreibe(s: String): Unit = println(s)
trait MitZeitstempel extends Ausgabe:
override def schreibe(s: String): Unit =
super.schreibe(s"[${java.time.LocalTime.now.withNano(0)}] $s")
trait InGrossbuchstaben extends Ausgabe:
override def schreibe(s: String): Unit = super.schreibe(s.toUpperCase)
object Log extends Ausgabe, MitZeitstempel, InGrossbuchstaben
Log.schreibe("server gestartet")
// [14:03:12] SERVER GESTARTETInGrossbuchstaben kommt zuerst dran (ganz rechts), reicht an MitZeitstempel weiter, das schließlich an Ausgabe. Vertauschen Sie die beiden Traits, wird zuerst der Zeitstempel angefügt und danach alles in Großbuchstaben umgewandelt. Hier sieht das Ergebnis gleich aus, weil der Stempel nur Ziffern enthält – bei einem Stempel mit Monatsnamen wäre der Unterschied sichtbar. Die Reihenfolge der Traits ist also Teil der Bedeutung.
Trait-Parameter
Seit Scala 3 dürfen Traits Parameter haben:
trait Sprecher(sprache: String):
def vorstellen = s"Ich spreche $sprache."
class Dolmetscherin extends Sprecher("Italienisch")sealed traits: geschlossene Hierarchien
Mit sealed legen Sie fest, dass alle direkten Untertypen in derselben Datei stehen müssen. Der Compiler kennt dann alle Fälle und warnt, wenn ein Pattern Match einen vergisst:
sealed trait Zahlung
case class Bar(betrag: BigDecimal) extends Zahlung
case class Karte(betrag: BigDecimal, nummer: String) extends Zahlung
case object Gutschein extends Zahlung
def beschreibe(z: Zahlung): String = z match
case Bar(b) => s"$b € bar"
case Karte(b, n) => s"$b € mit Karte …${n.takeRight(4)}"
case Gutschein => "per Gutschein"Dieses Muster – ein sealed trait mit case classes als Varianten – nennt man algebraischen Datentyp. In Scala 3 lässt es sich oft noch knapper als enum schreiben:
enum Zahlung:
case Bar(betrag: BigDecimal)
case Karte(betrag: BigDecimal, nummer: String)
case GutscheinTraits und Java-Interfaces
Traits werden in Java-Interfaces mit Default-Methoden übersetzt, sofern sie keine Felder haben. Deshalb kann Scala-Code Java-Interfaces direkt implementieren und umgekehrt. Wer Scala-Bibliotheken für Java-Nutzer schreibt, hält Traits deshalb gern schlank.