scalatutorial.de

Stufe 3Objekte und Typen · Lektion 14 von 34

Traits

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

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

 Scala 3
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):

 Scala 3
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.

Mehrere Traits in einer Klasse und die Reihenfolge der Linearisierungclass EnteSchwimmtFliegtQuaktTierclass Ente extends Tier, Schwimmt, Fliegt, QuaktSuchreihenfolge: Ente → Quakt → Fliegt → Schwimmt → Tier
Die Suchreihenfolge verläuft von rechts nach links bis zur Basis.

Dadurch lassen sich Traits stapeln, jeder fügt seine Schicht hinzu:

 Scala 3
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 GESTARTET

InGrossbuchstaben 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:

 Scala 3
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:

 Scala 3
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:

 Scala 3
enum Zahlung:
  case Bar(betrag: BigDecimal)
  case Karte(betrag: BigDecimal, nummer: String)
  case Gutschein

Traits 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.

Passt dazu