Kapitel 4. Strukturen kontrollieren

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Wie der Name schon sagt, bieten Kontrollstrukturen Programmierern eine Möglichkeit, den Ablauf eines Programms zu steuern. Sie sind ein grundlegendes Merkmal von Programmiersprachen, mit denen du Entscheidungen treffen und Schleifen bilden kannst.

Bevor ich 2010 Scala lernte, dachte ich, dass Kontrollstrukturen wie if/then Anweisungen sowie for und while Schleifen relativ langweilig sind, aber nur, weil ich nicht wusste, dass es auch anders geht. Heute weiß ich, dass sie ein entscheidendes Merkmal von Programmiersprachen sind.

Die Kontrollstrukturen von Scala sind:

  • for Schleifen und for Ausdrücke

  • if/then/else if Ausdrücke

  • match Ausdrücke (Mustervergleich)

  • try/catch/finally Blöcke

  • while Schleifen

Im Folgenden stelle ich sie kurz vor und zeige dir dann in den Rezepten, wie du ihre Funktionen nutzen kannst.

for-Schleifen und for-Ausdrücke

In ihrer grundlegendsten Anwendung bieten for Schleifen eine Möglichkeit, über eine Sammlung zu iterieren und die Elemente der Sammlung zu bearbeiten:

for i <- List(1, 2, 3) do println(i)

Aber das ist nur ein grundlegender Anwendungsfall. for Schleifen können auch Guards enthalten - eingebettete ifAnweisungen:

for
    i <- 1 to 10
    if i > 3
    if i < 6
do
    println(i)

Mit dem Schlüsselwort yield werden aus for Schleifen auch for Ausdrücke - Schleifen, die ein Ergebnis liefern:

val listOfInts = for
    i <- 1 to 10
    if i > 3
    if i < 6
yield
    i * 10

Nachdem diese Schleife gelaufen ist, ist listOfInts ein Vector(40, 50). Die Wächter innerhalb der Schleife filtern alle Werte außer 4 und 5 heraus, und diese Werte werden dann im yield Block mit 10 multipliziert.

Viele weitere Details über for Schleifen und Ausdrücke werden in den ersten Rezepten in diesem Kapitel behandelt.

if/then/else-if Ausdrücke

Während for Schleifen und Ausdrücke es dir ermöglichen, eine Sammlung zu durchlaufen, bieten if/then/else Ausdrücke eine Möglichkeit, Verzweigungsentscheidungen zu treffen. In Scala 3 hat sich die bevorzugte Syntax geändert und sieht nun wie folgt aus:

val absValue = if a < 0 then -a else a

def compare(a: Int, b: Int): Int =
    if a < b then
        -1
    else if a == b then
        0
    else
        1
end compare

Wie in diesen beiden Beispielen gezeigt wird, ist ein if Ausdruck tatsächlich ein Ausdruck, der einen Wert zurückgibt. (Ausdrücke werden in Rezept 4.5 behandelt.)

Ausdrücke und Mustervergleiche abgleichen

match Ausdrücke und Mustervergleiche sind ein entscheidendes Merkmal von Scala, und die Demonstration ihrer Fähigkeiten nimmt den größten Teil dieses Kapitels ein. Wie Ausdrücke geben auch Ausdrücke Werte zurück, sodass du sie als Körper einer Methode verwenden kannst. Diese Methode ähnelt zum Beispiel der Version der Programmiersprache Perl, die und heißt: if match true false

def isTrue(a: Matchable): Boolean = a match
    case false | 0 | "" => false
    case _ => true

Wenn isTrue eine 0 oder eine leere Zeichenkette erhält, wird false zurückgegeben, andernfalls wird true zurückgegeben. In diesem Kapitel werden zehn Rezepte verwendet, um die Funktionen der match Ausdrücke zu erläutern.

try/catch/finally-Blöcke

Die Blöcke try/catch/finally in Scala sind ähnlich wie in Java, aber die Syntax ist etwas anders, vor allem weil der Block catch mit einem Ausdruck match übereinstimmt:

try
    // some exception-throwing code here
catch
    case e1: Exception1Type => // handle that exception
    case e2: Exception2Type => // handle that exception
finally
    // close your resources and do anything else necessary here

Wie if und match ist auch try ein Ausdruck, der einen Wert zurückgibt. Du kannst also einen Code wie diesen schreiben, um String in Int umzuwandeln:

def toInt(s: String): Option[Int] =
    try
        Some(s.toInt)
    catch
        case e: NumberFormatException => None

Diese Beispiele zeigen, wie toInt funktioniert:

toInt("1")    // Option[Int] = Some(1)
toInt("Yo")   // Option[Int] = None

Rezept 4.16 enthält weitere Informationen über try/catch Blöcke.

while-Schleifen

Wenn es um while Schleifen geht, wirst du feststellen, dass sie in Scala nur selten verwendet werden. Das liegt daran, dass while Schleifen hauptsächlich für Seiteneffekte verwendet werden, wie z.B. das Aktualisieren von veränderbarenVariablen und das Drucken mit println. Diese Dinge kannst du auch mit for Schleifen und der foreach Methode für Collections machen. Falls du dennoch eine Schleife verwenden musst, sieht ihre Syntax wie folgt aus:

while
    i < 10
do
    println(i)
    i += 1

while Schleifen werden kurz in Rezept 4.1 behandelt.

Schließlich kannst du dank einer Kombination verschiedener Scala-Funktionen deine eigenen Kontrollstrukturen erstellen. Diese Möglichkeiten werden in Rezept 4.17 erläutert.

Kontrollstrukturen als bestimmendes Merkmal von Programmiersprachen

Ende 2020 hatte ich das Glück, das Scala 3-Buch auf der offiziellen Scala-Dokumentations-Website mitzuschreiben, einschließlich dieser dreiKapitel:

Als ich vorhin sagte, dass Kontrollstrukturen ein "definierendes Merkmal von Programmiersprachen" sind, meinte ich damit unter anderem, dass mir nach dem Schreiben dieser Kapitel klar wurde, wie leistungsstark die Funktionen in diesem Kapitel sind und wie konsistent Scala im Vergleich zu anderen Programmiersprachen ist. Diese Konsistenz ist eine der Eigenschaften, die die Verwendung von Scala zu einem Vergnügen machen.

4.1 Schleifen über Datenstrukturen mit for

Problem

Du willst die Elemente in einer Sammlung wie in einer herkömmlichen for Schleife durchlaufen.

Lösung

Es gibt viele Möglichkeiten, Schleifen über Scala-Sammlungen zu ziehen, darunter for Schleifen, while Schleifen und Sammelmethoden wie foreach, map, flatMap und andere. Diese Lösung konzentriert sich hauptsächlich auf die for Schleife.

Gegeben eine einfache Liste:

val fruits = List("apple", "banana", "orange")

kannst du eine Schleife über die Elemente in der Liste machen und sie so ausdrucken:

scala> for f <- fruits do println(f)
apple
banana
orange

Der gleiche Ansatz funktioniert für alle Sequenzen, einschließlich List, Seq, Vector, Array, ArrayBuffer, etc.

Wenn dein Algorithmus mehrere Zeilen benötigt, verwende die gleiche for Schleifensyntax und führe deine Arbeit in einem Block innerhalb geschweifter Klammern aus:

scala> for f <- fruits do
     |      // imagine this requires multiple lines
     |      val s = f.toUpperCase
     |      println(s)
APPLE
BANANA
ORANGE

For-Loop-Zähler

Wenn du innerhalb einer for Schleife Zugriff auf einen Zähler brauchst, wende eine der folgenden Methoden an. Erstens kannst du auf die Elemente in einer Folge mit einem Zähler wie folgt zugreifen:

for i <- 0 until fruits.length do
    println(s"$i is ${fruits(i)}")

Diese Schleife ergibt diese Ausgabe:

0 is apple
1 is banana
2 is orange

Du musst nur selten auf Sequenzelemente über ihren Index zugreifen, aber wenn, dann ist das eine mögliche Vorgehensweise. Scala-Sammlungen bieten auch eine zipWithIndex Methode, mit der du einen Schleifenzähler erstellen kannst:

for (fruit, index) <- fruits.zipWithIndex do
    println(s"$index is $fruit")

Die Ausgabe ist:

0 is apple
1 is banana
2 is orange

Stromerzeuger

Das folgende Beispiel zeigt, wie du mit Range eine Schleife dreimal ausführen kannst:

scala> for i <- 1 to 3 do println(i)
1
2
3

Der 1 to 3 Teil der Schleife erstellt eine Range, wie in der REPL gezeigt:

scala> 1 to 3
res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3)

Die Verwendung von Range auf diese Weise wird als Generator bezeichnet. Rezept 4.2 zeigt, wie du mit dieser Technik mehrere Schleifenzähler erstellen kannst.

Schleifen über eine Karte

Wenn du über Schlüssel und Werte in einer Map iterierst, finde ich, dass dies die prägnanteste und lesbarste for Schleife ist:

val names = Map(
    "firstName" -> "Robert",
    "lastName" -> "Goren"
)

for (k,v) <- names do println(s"key: $k, value: $v")

Die REPL zeigt ihre Ausgabe an:

scala> for (k,v) <- names do println(s"key: $k, value: $v")
key: firstName, value: Robert
key: lastName, value: Goren

Diskussion

Da ich auf einen funktionalen Programmierstil umgestiegen bin, habe ich seit einigen Jahren keine while Schleife mehr verwendet, aber die REPL zeigt, wie sie funktioniert:

scala> var i = 0
i: Int = 0

scala> while i < 3 do
     |     println(i)
     |     i += 1
0
1
2

while Schleifen werden in der Regel für Nebeneffekte verwendet, z. B. um eine veränderbare Variable wie i zu aktualisieren und die Ausgabe nach außen zu schreiben. Da sich mein Code immer mehr der reinen funktionalen Programmierung annähert - bei der es keinen veränderbaren Zustand gibt -, habe ich keinen Bedarf mehr für sie.

Trotzdem werden while -Schleifen bei der objektorientierten Programmierung häufig verwendet, und dieses Beispiel zeigt ihre Syntax. Eine while -Schleife kann auch in mehreren Zeilen geschrieben werden, wie hier:

while
    i < 10
do
    println(i)
    i += 1

Sammelmethoden wie foreach

In gewisser Weise erinnert mich Scala an den Perl-Slogan "Es gibt mehr als einen Weg, etwas zu tun", und die Iteration über eine Sammlung bietet einige großartige Beispiele dafür. Bei der Fülle an Methoden, die für Sammlungen zur Verfügung stehen, ist es wichtig zu wissen, dass eine for Schleife vielleicht nicht einmal der beste Ansatz für ein bestimmtes Problem ist; die Methoden foreach, map, flatMap, collect, reduce, usw. können oft verwendet werden, um dein Problem zu lösen, ohne dass eine explizite for Schleife erforderlich ist.

Wenn du zum Beispiel mit einer Sammlung arbeitest, kannst du auch über jedes Element iterieren, indem du die Methode foreach für die Sammlung aufrufst:

scala> fruits.foreach(println)
apple
banana
orange

Wenn du einen Algorithmus hast, den du auf jedes Element in der Sammlung anwenden willst, übergibst du die anonyme Funktion einfach an foreach:

scala> fruits.foreach(e => println(e.toUpperCase))
APPLE
BANANA
ORANGE

Wie bei der for Schleife gilt: Wenn dein Algorithmus mehrere Zeilen benötigt, führe deine Arbeit in einem Block aus:

scala> fruits.foreach { e =>
     |     val s = e.toUpperCase
     |     println(s)
     | }
APPLE
BANANA
ORANGE

Siehe auch

Die Theorie, die hinter der Funktionsweise der for Schleifen steckt, ist sehr interessant und es kann hilfreich sein, sie zu kennen, wenn du Fortschritte machst. Ich habe in diesen Artikeln ausführlich darüber geschrieben:

4.2 Verwendung von for-Schleifen mit mehreren Zählern

Problem

Du möchtest eine Schleife mit mehreren Zählern erstellen, z.B. wenn du über ein mehrdimensionales Array iterierst.

Lösung

Du kannst eine for Schleife mit zwei Zählern wie folgt erstellen:

scala> for i <- 1 to 2; j <- 1 to 2 do println(s"i = $i, j = $j")
i = 1, j = 1
i = 1, j = 2
i = 2, j = 1
i = 2, j = 2

Beachte, dass i auf 1 gesetzt wird, eine Schleife durch die Elemente in j gemacht wird, dann i auf 2 gesetzt wird und der Vorgang wiederholt wird.

Dieser Ansatz funktioniert gut bei kleinen Beispielen, aber wenn dein Code umfangreicher wird, ist dies der bevorzugte Stil:

for
    i <- 1 to 3
    j <- 1 to 5
    k <- 1 to 10 by 2
do
    println(s"i = $i, j = $j, k = $k")

Dieser Ansatz ist nützlich, wenn du eine Schleife über ein mehrdimensionales Array ziehst. Angenommen, du erstellst und füllst ein kleines zweidimensionales Array wie dieses:

val a = Array.ofDim[Int](2,2)
a(0)(0) = 0
a(0)(1) = 1
a(1)(0) = 2
a(1)(1) = 3

kannst du jedes Array-Element wie folgt ausgeben:

scala> for
     |     i <- 0 to 1
     |     j <- 0 to 1
     | do
     |     println(s"($i)($j) = ${a(i)(j)}")
(0)(0) = 0
(0)(1) = 1
(1)(0) = 2
(1)(1) = 3

Diskussion

Wie in Rezept 15.2, "Erstellen von Bereichen", gezeigt , erstellt die 1 to 5 Syntax eine Range:

scala> 1 to 5
val res0: scala.collection.immutable.Range.Inclusive = Range 1 to 5

Bereiche sind für viele Zwecke geeignet, und Bereiche, die mit dem Symbol <- in for Schleifen erstellt werden, werden als Generatoren bezeichnet. Wie gezeigt, kannst du ganz einfach mehrere Generatoren in einer Schleife verwenden.

4.3 Verwendung einer for-Schleife mit eingebetteten if-Anweisungen (Guards)

Problem

Du möchtest eine oder mehrere bedingte Klauseln zu einer for Schleife hinzufügen, normalerweise um einige Elemente in einer Sammlung herauszufiltern, während du die anderen bearbeitest.

Lösung

Füge eine oder mehrere if Anweisungen nach deinem Generator ein, etwa so:

for
    i <- 1 to 10
    if i % 2 == 0
do
    print(s"$i ")

// output: 2 4 6 8 10

Diese if Anweisungen werden als Filter, Filterausdrücke oder Wächter bezeichnet, und du kannst so viele Wächter verwenden, wie du für das jeweilige Problem brauchst. Diese Schleife zeigt einen harten Weg, um die Zahl 4 zu drucken:

for
    i <- 1 to 10
    if i > 3
    if i < 6
    if i % 2 == 0
do
    println(i)

Diskussion

Es ist immer noch möglich, for Schleifen mit if Ausdrücken in einem älteren Stil zu schreiben. Zum Beispiel mit diesem Code:

import java.io.File
val dir = File(".")
val files: Array[java.io.File] = dir.listFiles()

könntest du theoretisch eine for Schleife in einem Stil schreiben, der an C und Java erinnert:

// a C/Java style of writing a 'for' loop
for (file <- files) {
    if (file.isFile && file.getName.endsWith(".scala")) {
        println(s"Scala file: $file")
    }
}

Wenn du dich aber erst einmal mit Scalas for Schleifensyntax vertraut gemacht hast, wirst du feststellen, dass sie den Code lesbarer macht, weil sie die Schleifen und Filter von der Geschäftslogik trennt:

for
    // loop and filter
    file <- files
    if file.isFile
    if file.getName.endsWith(".scala")
do
    // as much business logic here as needed
    println(s"Scala file: $file")

Da Guards in der Regel dazu gedacht sind, Sammlungen zu filtern, solltest du je nach Bedarf eine der vielen Filtermethoden verwenden, die für Sammlungen zur Verfügung stehen -filter, take, drop, usw. - anstatt einer for Schleife. In Kapitel 11 findest du Beispiele für diese Methoden.

4.4 Erstellen einer neuen Sammlung aus einer bestehenden Sammlung mit for/yield

Problem

Du möchtest aus einer bestehenden Sammlung eine neue Sammlung erstellen, indem du einen Algorithmus (und möglicherweise eine oder mehrere Wachen) auf jedes Element der ursprünglichen Sammlung anwendest.

Lösung

Verwende eine yield Anweisung mit einer for Schleife, um eine neue Sammlung aus einer bestehenden Sammlung zu erstellen. Nehmen wir zum Beispiel ein Array mit klein geschriebenen Strings:

scala> val names = List("chris", "ed", "maurice")
val names: List[String] = List(chris, ed, maurice)

kannst du ein neues Array mit großgeschriebenen Zeichenketten erstellen, indem du yield mit einer for Schleife und einem einfachen Algorithmus kombinierst:

scala> val capNames = for name <- names yield name.capitalize
val capNames: List[String] = List(Chris, Ed, Maurice)

Die Verwendung einer for -Schleife mit einer yield -Anweisung wird als For-Comprehension bezeichnet.

Wenn dein Algorithmus mehrere Codezeilen erfordert, führe die Arbeit in einem Block nach dem Schlüsselwort yield aus und gib den Typ der resultierenden Variablen manuell an oder nicht:

// [1] declare the type of `lengths`
val lengths: List[Int] = for name <- names yield
    // imagine that this body requires multiple lines of code
    name.length

// [2] don’t declare the type of `lengths`
val lengths = for name <- names yield
    // imagine that this body requires multiple lines of code
    name.length

Beide Ansätze führen zu demselben Ergebnis:

List[Int] = List(5, 2, 7)

Beide Teile deines for Verständnisses (auch bekannt als for Ausdruck) können so kompliziert wie nötig sein. Hier ist ein größeres Beispiel:

val xs = List(1,2,3)
val ys = List(4,5,6)
val zs = List(7,8,9)

val a = for
    x <- xs
    if x > 2
    y <- ys
    z <- zs
    if y * z < 45
yield
    val b = x + y
    val c = b * z
    c

Das for Verständnis führt zu folgendem Ergebnis:

a: List[Int] = List(49, 56, 63, 56, 64, 63)

Eine for comprehension kann sogar der komplette Körper einer Methode sein:

def between3and10(xs: List[Int]): List[Int] =
    for
        x <- xs
        if x >= 3
        if x <= 10
    yield x

between3and10(List(1,3,7,11))   // List(3, 7)

Diskussion

Wenn du zum ersten Mal yield mit einer for Schleife verwendest, kannst du dir die Schleife wie folgt vorstellen:

  1. Zu Beginn der Schleife for/yield wird sofort eine neue leere Sammlung erstellt, die denselben Typ hat wie die Eingabesammlung. Wenn der Eingabetyp zum Beispiel Vector ist, ist der Ausgabetyp auch Vector. Du kannst dir diese neue Sammlung wie einen leeren Eimer vorstellen.

  2. Bei jeder Iteration der for Schleife kann ein neues Ausgangselement aus dem aktuellen Element der Eingangssammlung erstellt werden. Wenn das Ausgabeelement erstellt ist, wird es in den Bucket gelegt.

  3. Wenn die Schleife zu Ende ist, wird der gesamte Inhalt des Eimers zurückgegeben.

Das ist eine Vereinfachung, aber ich finde es hilfreich, um den Prozess zu erklären.

Beachte, dass das Schreiben eines for Ausdrucks ohne Guard genauso ist, wie der Aufruf der map Methode für eine Sammlung.

Das folgende for Verständnis wandelt zum Beispiel alle Zeichenketten in der fruits Sammlung in Großbuchstaben um:

scala> val namesUpper = for n <- names yield n.toUpperCase
val namesUpper: List[String] = List(CHRIS, ED, MAURICE)

Der Aufruf der Methode map für die Sammlung bewirkt das Gleiche:

scala> val namesUpper = names.map(_.toUpperCase)
val namesUpper: List[String] = List(CHRIS, ED, MAURICE)

Als ich anfing, Scala zu lernen, schrieb ich meinen gesamten Code mit for/yield Ausdrücken, bis ich eines Tages feststellte, dass die Verwendung von for/yield ohne Guard das Gleiche ist wie die Verwendung von map.

Siehe auch

4.5 Das if-Konstrukt wie einen ternären Operator verwenden

Problem

Du bist mit der speziellen ternären Operator-Syntax von Java vertraut:

int absValue = (a < 0) ? -a : a;

und du möchtest wissen, was die Entsprechung in Scala ist.

Lösung

Das ist ein kleines Trickproblem, denn im Gegensatz zu Java gibt es in Scala keinen speziellen ternären Operator; verwende einfach einen if/else/then Ausdruck:

val a = 1
val absValue = if a < 0 then -a else a

Da ein if Ausdruck einen Wert zurückgibt, kannst du ihn in eine Druckanweisung einbetten:

println(if a == 0 then "a" else "b")

Du kannst ihn auch in einem anderen Ausdruck verwenden, z. B. in diesem Teil einer hashCode Methode:

hash = hash * prime + (if name == null then 0 else name.hashCode)

Die Tatsache, dass if/else-Ausdrücke einen Wert zurückgeben, ermöglicht es dir auch, prägnante Methoden zu schreiben:

// Version 1: one-line style
def abs(x: Int) = if x >= 0 then x else -x
def max(a: Int, b: Int) = if a > b then a else b

// Version 2: the method body on a separate line, if you prefer
def abs(x: Int) =
    if x >= 0 then x else -x

def max(a: Int, b: Int) =
    if a > b then a else b

Diskussion

Auf der Java-Dokumentationsseite "Equality, Relational, and Conditional Operators" (Gleichheits-, Beziehungs- und Bedingungsoperatoren) steht, dass der Java-Bedingungsoperator ?: "als ternärer Operator bekannt ist, weil er drei Operanden verwendet".

Java benötigt hier eine eigene Syntax, weil das Java if/else Konstrukt eine Anweisung ist; es hat keinen Rückgabewert und wird nur für Seiteneffekte verwendet, wie z.B. die Aktualisierung veränderbarer Felder. Da es sich bei if/else/then in Scala um einen Ausdruck handelt, wird kein spezieller Operator benötigt. In Rezept 24.3, "Schreiben von Ausdrücken (anstelle von Anweisungen)", findest du weitere Informationen zu Anweisungen und Ausdrücken.

Arity

Das Wort ternär hat mit der Arität von Funktionen zu tun. Auf der Wikipedia-Seite "Arität" steht: "In der Logik, Mathematik und Informatik ist die Arität einer Funktion oder Operation die Anzahl der Argumente oder Operanden, die die Funktion benötigt. Ein unärer Operator benötigt einen Operanden, ein binärer Operator benötigt zwei Operanden und ein ternärer Operator benötigt drei Operanden.

4.6 Einen Match-Ausdruck wie eine switch-Anweisung verwenden

Problem

Du hast eine Situation, in der du so etwas wie eine einfache ganzzahlige Java-Anweisung switch erstellen möchtest, z. B. die Tage einer Woche, die Monate eines Jahres und andere Situationen, in denen eine ganze Zahl einem Ergebnis entspricht.

Lösung

Um einen Scala match Ausdruck wie eine einfache, ganzzahlige switch Anweisung zu verwenden, verwende diesen Ansatz:

import scala.annotation.switch

// `i` is an integer
(i: @switch) match
    case 0 => println("Sunday")
    case 1 => println("Monday")
    case 2 => println("Tuesday")
    case 3 => println("Wednesday")
    case 4 => println("Thursday")
    case 5 => println("Friday")
    case 6 => println("Saturday")
    // catch the default with a variable so you can print it
    case whoa  => println(s"Unexpected case: ${whoa.toString}")

Dieses Beispiel zeigt, wie man eine Nebeneffekt-Aktion (println) basierend auf einer Übereinstimmung erzeugt. Ein funktionalerer Ansatz ist es, einen Wert aus einem match Ausdruck zurückzugeben:

import scala.annotation.switch

// `i` is an integer
val day = (i: @switch) match
    case 0 => "Sunday"
    case 1 => "Monday"
    case 2 => "Tuesday"
    case 3 => "Wednesday"
    case 4 => "Thursday"
    case 5 => "Friday"
    case 6 => "Saturday"
    case _ => "invalid day"   // the default, catch-all

Die @switch-Anmerkung

Wenn du einfache match Ausdrücke wie diesen schreibst, solltest du die @switch Annotation verwenden, wie in der Abbildung gezeigt. Diese Annotation gibt bei der Kompilierung eine Warnung aus, wenn der Schalter nicht in eine tableswitch oder lookupswitch kompiliert werden kann. Die Kompilierung deines Match-Ausdrucks in eine tableswitch oder lookupswitch ist besser für die Leistung, weil sie zu einer Verzweigungstabelle und nicht zu einem Entscheidungsbaum führt. Wenn dem Ausdruck ein Wert gegeben wird, kann er direkt zum Ergebnis springen, anstatt sich durch den Entscheidungsbaum zu arbeiten.

In der Scala @switch Annotation-Dokumentation steht:

Wenn [diese Anmerkung] vorhanden ist, prüft der Compiler, ob die Übereinstimmung zu einem Tableswitch oder Lookupswitch kompiliert wurde, und gibt einen Fehler aus, wenn sie stattdessen zu einer Reihe von bedingten Ausdrücken kompiliert wurde

Die Wirkung der Annotation @switch wird anhand eines einfachen Beispiels demonstriert. Platziere zunächst den folgenden Code in einer Datei namens SwitchDemo.scala:

// Version 1 - compiles to a tableswitch
import scala.annotation.switch

class SwitchDemo:
    val i = 1
    val x = (i: @switch) match
        case 1 => "One"
        case 2 => "Two"
        case 3 => "Three"
        case _ => "Other"

Kompiliere den Code dann wie gewohnt:

$ scalac SwitchDemo.scala

Das Kompilieren dieser Klasse führt zu keinen Warnungen und erzeugt die Ausgabedatei SwitchDemo.class. Disassembliere diese Datei mit diesem javap Befehl:

$ javap -c SwitchDemo

Die Ausgabe dieses Befehls zeigt eine tableswitch, wie diese:

16: tableswitch   { // 1 to 3
             1: 44
             2: 52
             3: 60
       default: 68
  }

Das zeigt, dass Scala in der Lage war, deinen match Ausdruck auf tableswitch zu optimieren. (Das ist eine gute Sache.)

Als Nächstes nimmst du eine kleine Änderung am Code vor, indem du das Integer-Literal 1 durch einen Wert ersetzst:

import scala.annotation.switch

// Version 2 - leads to a compiler warning
class SwitchDemo:
    val i = 1
    val one = 1               // added
    val x = (i: @switch) match
        case one  => "One"    // replaced the '1'
        case 2    => "Two"
        case 3    => "Three"
        case _    => "Other"

Kompiliere den Code noch einmal mit scalac, aber gleich darauf wirst du eine Warnmeldung sehen:

$ scalac SwitchDemo.scala
SwitchDemo.scala:7: warning: could not emit switch for @switch annotated match
  val x = (i: @switch) match {
               ^
one warning found

Diese Warnmeldung bedeutet, dass weder ein tableswitch noch ein lookupswitch für den Ausdruck match erstellt werden konnte. Du kannst dies bestätigen, indem du den Befehl javap in der erzeugten Datei SwitchDemo.class ausführst. Wenn du dir die Ausgabe ansiehst, wirst du feststellen, dass die tableswitch aus dem vorherigen Beispiel verschwunden ist.

In seinem Buch Scala in Depth (Manning) erklärt Joshua Suereth, dass die folgenden Bedingungen erfüllt sein müssen, damit Scala die tableswitch Optimierung anwenden kann:

  • Der übereinstimmende Wert muss eine bekannte ganze Zahl sein.

  • Der übereinstimmende Ausdruck muss "einfach" sein. Er darf keine Typüberprüfungen, if Anweisungen oder Extraktoren enthalten.

  • Der Wert des Ausdrucks muss zur Kompilierzeit verfügbar sein.

  • Es sollte mehr als zwei case Aussagen geben.

Diskussion

Die Beispiele in der Lösung zeigen zwei Möglichkeiten, wie du den Standardfall "catch all" behandeln kannst. Erstens: Wenn dir der Wert der Standardübereinstimmung egal ist, kannst du ihn mit dem Platzhalter _ auffangen:

case _ => println("Got a default match")

Umgekehrt, wenn du dich dafür interessierst, was auf die Standardübereinstimmung gefallen ist, weise ihr einen Variablennamen zu. Diese Variable kannst du dann auf der rechten Seite des Ausdrucks verwenden:

case default => println(default)

Ein Name wie default ist oft am sinnvollsten, aber du kannst jeden legalen Namen für die Variable verwenden:

case oops => println(oops)

Es ist wichtig zu wissen, dass du eine MatchError erzeugen kannst, wenn du den Standardfall nicht behandelst. Mit diesem match Ausdruck:

i match
    case 0 => println("0 received")
    case 1 => println("1 is good, too")

Wenn i ein anderer Wert als 0 oder 1 ist, löst der Ausdruck einen MatchError aus:

scala.MatchError: 42 (of class java.lang.Integer)
  at .<init>(<console>:9)
  at .<clinit>(<console>)
    much more error output here ...

Wenn du also nicht absichtlich eine Teilfunktion schreibst, solltest du den Standardfall behandeln.

Brauchst du wirklich einen Match-Ausdruck?

Beachte, dass du für Beispiele wie dieses keinen Match-Ausdruck brauchst. Wenn du zum Beispiel nur einen Wert auf einen anderen abbildest, kann es besser sein, einen Map zu verwenden:

val days = Map(
    0 -> "Sunday",
    1 -> "Monday",
    2 -> "Tuesday",
    3 -> "Wednesday",
    4 -> "Thursday",
    5 -> "Friday",
    6 -> "Saturday"
)

println(days(0))   // prints "Sunday"

Siehe auch

4.7 Mehrere Bedingungen miteiner Case-Anweisung abgleichen

Problem

Du hast eine Situation, in der mehrere match Bedingungen die Ausführung derselben Geschäftslogik erfordern. Anstatt deine Geschäftslogik für jeden Fall zu wiederholen, möchtest du eine Kopie der Geschäftslogik für die passenden Bedingungen verwenden.

Lösung

Setze die Übereinstimmungsbedingungen, die dieselbe Geschäftslogik aufrufen, in eine Zeile, getrennt durch das Zeichen | (Pipe):

// `i` is an Int
i match
    case 1 | 3 | 5 | 7 | 9 => println("odd")
    case 2 | 4 | 6 | 8 | 10 => println("even")
    case _ => println("too big")

Die gleiche Syntax funktioniert auch mit Strings und anderen Typen. Hier ist ein Beispiel, das auf einer String Übereinstimmung basiert:

val cmd = "stop"
cmd match
    case "start" | "go" => println("starting")
    case "stop" | "quit" | "exit" => println("stopping")
    case _ => println("doing nothing")

Dieses Beispiel zeigt, wie du mehrere Objekte in jeder case Anweisung abgleichen kannst:

enum Command:
    case Start, Go, Stop, Whoa

import Command.*
def executeCommand(cmd: Command): Unit = cmd match
    case Start | Go => println("start")
    case Stop | Whoa => println("stop")

Wie gezeigt, kann die Möglichkeit, mehrere mögliche Übereinstimmungen für jede case Anweisung zu definieren, deinen Code vereinfachen.

Siehe auch

4.8 Das Ergebnis eines Match-Ausdruckseiner Variablen zuweisen

Problem

Du willst einen Wert aus einem match Ausdruck zurückgeben und ihn einer Variablen zuweisen oder einen match Ausdruck als Körper einer Methode verwenden.

Lösung

Um das Ergebnis eines match Ausdrucks einer Variablen zuzuweisen, fügst du die Variablenzuweisung vor dem Ausdruck ein, wie bei der Variablen evenOrOdd in diesem Beispiel:

val someNumber = scala.util.Random.nextInt()
val evenOrOdd = someNumber match
    case 1 | 3 | 5 | 7 | 9 => "odd"
    case 2 | 4 | 6 | 8 | 10 => "even"
    case _ => "other"

Dieser Ansatz wird häufig verwendet, um kurze Methoden oder Funktionen zu erstellen. Die folgende Methode implementiert zum Beispiel die Perl-Definitionen von true und false:

def isTrue(a: Matchable): Boolean = a match
    case false | 0 | "" => false
    case _ => true

Diskussion

Du hast vielleicht gehört, dass Scala eine ausdrucksorientierte Programmiersprache (EOP) ist. EOP bedeutet, dass jedes Konstrukt ein Ausdruck ist, einen Wert liefert und keine Nebenwirkung hat. Im Gegensatz zu anderen Sprachen gibt in Scala jedes Konstrukt wie if, match, for und try einen Wert zurück. In Rezept 24.3, "Ausdrücke schreiben (statt Anweisungen)", findest du weitere Informationen.

4.9 Zugriff auf den Wert des Standardfalls in einemMatch-Ausdruck

Problem

Du willst auf den Wert des Standardfalls "catch all" zugreifen, wenn du einen Match-Ausdruck verwendest, aber du kannst nicht auf den Wert zugreifen, wenn du ihn mit der _ Wildcard-Syntax abgleichst.

Lösung

Anstatt das Platzhalterzeichen _ zu verwenden, weise einem Variablennamen die Standardgroßschreibung zu:

i match
    case 0 => println("1")
    case 1 => println("2")
    case default => println(s"You gave me: $default")

Wenn du der Standardübereinstimmung einen Variablennamen gibst, kannst du auf die Variable auf der rechten Seite des Ausdrucks zugreifen.

Diskussion

Der Schlüssel zu diesem Rezept liegt in der Verwendung eines Variablennamens für die Standardübereinstimmung anstelle des üblichen _ Platzhalterzeichens. Der Name, den du vergibst, kann ein beliebiger legaler Variablenname sein. Anstatt default zu nennen, kannst du sie also auch anders nennen, zum Beispiel what:

i match
    case 0 => println("1")
    case 1 => println("2")
    case what => println(s"You gave me: $what" )

Es ist wichtig, eine Standardübereinstimmung anzugeben. Wenn du das nicht tust, kann es zu einer MatchError kommen:

scala> 3 match
     |     case 1 => println("one")
     |     case 2 => println("two")
     |     // no default match
scala.MatchError: 3 (of class java.lang.Integer)
many more lines of output ...

Siehe die Diskussion zu Rezept 4.6 für weitere Details MatchError.

4.10 Musterübereinstimmung in Match-Ausdrücken verwenden

Problem

Du musst ein oder mehrere Muster in einem match Ausdruck abgleichen. Das Muster kann ein Konstantenmuster, ein Variablenmuster, ein Konstruktormuster, ein Sequenzmuster, ein Tupelmuster oder ein Typmuster sein.

Lösung

Definiere eine case Anweisung für jedes Muster, das du abgleichen willst. Die folgende Methode zeigt Beispiele für viele verschiedene Arten von Mustern, die du in match Ausdrücken verwenden kannst:

def test(x: Matchable): String = x match

    // constant patterns
    case 0 => "zero"
    case true => "true"
    case "hello" => "you said 'hello'"
    case Nil => "an empty List"

    // sequence patterns
    case List(0, _, _) => "a 3-element list with 0 as the first element"
    case List(1, _*) => "list, starts with 1, has any number of elements"

    // tuples
    case (a, b) => s"got $a and $b"
    case (a, b, c) => s"got $a, $b, and $c"

    // constructor patterns
    case Person(first, "Alexander") => s"Alexander, first name = $first"
    case Dog("Zeus") => "found a dog named Zeus"

    // typed patterns
    case s: String => s"got a string: $s"
    case i: Int => s"got an int: $i"
    case f: Float => s"got a float: $f"
    case a: Array[Int] => s"array of int: ${a.mkString(",")}"
    case as: Array[String] => s"string array: ${as.mkString(",")}"
    case d: Dog => s"dog: ${d.name}"
    case list: List[_] => s"got a List: $list"
    case m: Map[_, _] => m.toString

    // the default wildcard pattern
    case _ => "Unknown"

end test

Der große Ausdruck match in dieser Methode zeigt die verschiedenen Kategorien von Mustern, die im Buch Programming in Scala beschrieben werden, darunter Konstantenmuster, Sequenzmuster, Tupelmuster, Konstruktormuster und typisierte Muster.

Der folgende Code demonstriert alle Fälle des Ausdrucks match, wobei die Ausgabe jedes Ausdrucks in den Kommentaren gezeigt wird. Beachte, dass die Methode println beim Import umbenannt wurde, damit die Beispiele übersichtlicher sind:

import System.out.{println => p}

case class Person(firstName: String, lastName: String)
case class Dog(name: String)

// trigger the constant patterns
p(test(0))               // zero
p(test(true))            // true
p(test("hello"))         // you said 'hello'
p(test(Nil))             // an empty List

// trigger the sequence patterns
p(test(List(0,1,2)))     // a 3-element list with 0 as the first element
p(test(List(1,2)))       // list, starts with 1, has any number of elements
p(test(List(1,2,3)))     // list, starts with 1, has any number of elements
p(test(Vector(1,2,3)))   // vector, starts w/ 1, has any number of elements

// trigger the tuple patterns
p(test((1,2)))                            // got 1 and 2
p(test((1,2,3)))                          // got 1, 2, and 3

// trigger the constructor patterns
p(test(Person("Melissa", "Alexander")))   // Alexander, first name = Melissa
p(test(Dog("Zeus")))                      // found a dog named Zeus

// trigger the typed patterns
p(test("Hello, world"))                   // got a string: Hello, world
p(test(42))                               // got an int: 42
p(test(42F))                              // got a float: 42.0
p(test(Array(1,2,3)))                     // array of int: 1,2,3
p(test(Array("coffee", "apple pie")))     // string array: coffee,apple pie
p(test(Dog("Fido")))                      // dog: Fido
p(test(List("apple", "banana")))          // got a List: List(apple, banana)
p(test(Map(1->"Al", 2->"Alexander")))     // Map(1 -> Al, 2 -> Alexander)

// trigger the wildcard pattern
p(test("33d"))                            // you gave me this string: 33d

Beachte, dass in dem Ausdruck match die Ausdrücke List und Map wie folgt geschrieben wurden:

case m: Map[_, _] => m.toString
case list: List[_] => s"thanks for the List: $list"

hätte stattdessen auch so geschrieben werden können:

case m: Map[A, B] => m.toString
case list: List[X] => s"thanks for the List: $list"

Ich bevorzuge die Unterstrich-Syntax, weil sie deutlich macht, dass ich mich nicht dafür interessiere, was in List oder Map gespeichert ist. Es kann sogar vorkommen, dass ich mich dafür interessiere, was in List oder Map gespeichert ist, aber wegen der Typlöschung in der JVM wird das zu einem schwierigen Problem.

Typ Löschung

Als ich dieses Beispiel zum ersten Mal geschrieben habe, habe ich den Ausdruck List wiefolgt geschrieben:

case l: List[Int] => "List"

Wenn du dich mit dem Löschen von Typen auf der Java-Plattform auskennst, weißt du vielleicht, dass das nicht funktioniert. Der Scala-Compiler weist dich freundlicherweise mit dieser Warnmeldung auf dieses Problem hin:

Test1.scala:7: warning: non-variable type argument Int in
type pattern List[Int] is unchecked since it is eliminated
by erasure case l: List[Int] => "List[Int]"
                   ^

Wenn du mit dem Löschen von Typen nicht vertraut bist, habe ich im Abschnitt "Siehe auch" dieses Rezepts einen Link zu einer Seite eingefügt, die beschreibt, wie es in der JVM funktioniert.

Diskussion

Wenn du diese Technik verwendest, erwartet deine Methode in der Regel eine Instanz, die von einer Basisklasse oder einem Trait erbt, und deine case Anweisungen verweisen dann auf Untertypen dieses Basistyps. Dies wurde in der Methode test abgeleitet, bei der jeder Scala-Typ ein Subtyp von Matchable ist. Der folgende Code zeigt ein offensichtlicheres Beispiel.

In meiner Blue Parrot-Anwendung, die entweder eine Tondatei abspielt oder den Text "spricht", der ihr in zufälligen Zeitabständen vorgegeben wird, habe ich eine Methode, die wie folgt aussieht:

import java.io.File

sealed trait RandomThing

case class RandomFile(f: File) extends RandomThing
case class RandomString(s: String) extends RandomThing

class RandomNoiseMaker:
    def makeRandomNoise(thing: RandomThing) = thing match
        case RandomFile(f) => playSoundFile(f)
        case RandomString(s) => speakText(s)

Die Methode makeRandomNoise ist so deklariert, dass sie einen Typ RandomThing annimmt, und der Ausdruck match behandelt die beiden Untertypen RandomFile und RandomString.

Muster

Der große Ausdruck match in der Lösung zeigt eine Reihe von Mustern, die im Buch Programming in Scala (das von Martin Odersky, dem Schöpfer der Sprache Scala, mitverfasst wurde) definiert sind. Zu den Mustern gehören:

  • Konstante Muster

  • Variable Muster

  • Konstruktionsmuster

  • Sequenzmuster

  • Tupel-Muster

  • Getippte Muster

  • Variabel-bindende Muster

Diese Muster werden in den folgenden Abschnitten kurz beschrieben.

Konstante Muster

Ein Konstantenmuster kann nur mit sich selbst übereinstimmen. Jedes Literal kann als Konstante verwendet werden. Wenn du ein 0 als Literal angibst, wird nur ein Int Wert von 0 abgeglichen. Beispiele sind:

case 0 => "zero"
case true => "true"
Variable Muster

Dies wurde in dem großen Match-Beispiel in der Lösung nicht gezeigt, aber ein Variablenmuster passt zu jedem Objekt, genau wie das _ Platzhalterzeichen. Scala bindet die Variable an das Objekt, so dass du die Variable auf der rechten Seite der case Anweisung verwenden kannst. Zum Beispiel kannst du am Ende eines match Ausdrucks das _ Platzhalterzeichen wie folgt verwenden, um alles andere zu erfassen:

case _ => s"Hmm, you gave me something ..."

Aber mit einem variablen Muster kannst du stattdessen dies schreiben:

case foo => s"Hmm, you gave me a $foo"

Siehe Rezept 4.9 für weitere Informationen.

Konstruktionsmuster

Mit dem Konstruktormuster kannst du einen Konstruktor in einer case Anweisung finden. Wie in den Beispielen gezeigt, kannst du im Konstruktormuster nach Bedarf Konstanten oder Variablenmuster angeben:

case Person(first, "Alexander") => s"found an Alexander, first name = $first"
case Dog("Zeus") => "found a dog named Zeus"
Sequenzmuster

Du kannst mit Sequenzen wie List, Array, Vector, usw. übereinstimmen. Verwende das Zeichen _, um für ein Element in der Sequenz zu stehen, und verwende _*, um für null oder mehr Elemente zu stehen, wie in den Beispielen gezeigt:

case List(0, _, _) => "a 3-element list with 0 as the first element"
case List(1, _*) => "list, starts with 1, has any number of elements"
case Vector(1, _*) => "vector, starts with 1, has any number of elements"
Tupel-Muster

Wie in den Beispielen gezeigt, kannst du Tupelmuster abgleichen und auf den Wert jedes Elements im Tupel zugreifen. Du kannst auch den Platzhalter _ verwenden, wenn du nicht an dem Wert eines Elements interessiert bist:

case (a, b, c) => s"3-elem tuple, with values $a, $b, and $c"
case (a, b, c, _) => s"4-elem tuple: got $a, $b, and $c"
Getippte Muster

Im folgenden Beispiel ist str: String ein typisiertes Muster und str ist eine Mustervariable:

case str: String => s"you gave me this string: $str"

Wie in den Beispielen gezeigt, kannst du auf die Mustervariable auf der rechten Seite des Ausdrucks zugreifen, nachdem du sie deklariert hast.

Variabel-bindende Muster

Manchmal möchtest du vielleicht eine Variable zu einem Muster hinzufügen. Das kannst du mit der folgenden allgemeinen Syntax tun:

case variableName @ pattern => ...

Dies wird als Variablen-Bindungsmuster bezeichnet. Wenn es verwendet wird, wird die Eingabevariable des Ausdrucks match mit dem Muster verglichen, und wenn sie übereinstimmt, wird die Eingabevariable an variableName gebunden.

Die Nützlichkeit dieser Methode wird am besten deutlich, wenn du dir das Problem vor Augen führst, das sie löst. Nehmen wir an, du hast das Muster List, das vorhin gezeigt wurde:

case List(1, _*) => "a list beginning with 1, having any number of elements"

Wie gezeigt, kannst du damit auf eine List zugreifen, deren erstes Element 1 ist, aber bisher wurde auf der rechten Seite des Ausdrucks nicht auf die List zugegriffen. Wenn du auf eine List zugreifst, weißt du, dass du das tun kannst:

case list: List[_] => s"thanks for the List: $list"

Es scheint also, dass du es mit einem Sequenzmuster versuchen solltest:

case list: List(1, _*) => s"thanks for the List: $list"

Leider schlägt dies mit folgendem Compiler-Fehler fehl:

Test2.scala:22: error: '=>' expected but '(' found.
    case list: List(1, _*) => s"thanks for the List: $list"
                   ^
one error found

Die Lösung für dieses Problem besteht darin, das Sequenzmuster um ein Variablenbindungsmuster zu erweitern:

case list @ List(1, _*) => s"$list"

Dieser Code lässt sich kompilieren und funktioniert wie erwartet, sodass du Zugriff auf die List auf der rechten Seite der Anweisung hast.

Der folgende Code demonstriert dieses Beispiel und die Nützlichkeit dieses Ansatzes:

case class Person(firstName: String, lastName: String)

def matchType(x: Matchable): String = x match
    //case x: List(1, _*) => s"$x"   // doesn’t compile
    case x @ List(1, _*) => s"$x"    // prints the list

    //case Some(_) => "got a Some"   // works, but can’t access the Some
    //case Some(x) => s"$x"          // returns "foo"
    case x @ Some(_) => s"$x"        // returns "Some(foo)"

    case p @ Person(first, "Doe") => s"$p" // returns "Person(John,Doe)"
end matchType

@main def test2 =
    println(matchType(List(1,2,3)))             // prints "List(1, 2, 3)"
    println(matchType(Some("foo")))             // prints "Some(foo)"
    println(matchType(Person("John", "Doe")))   // prints "Person(John,Doe)"

In den beiden List Beispielen innerhalb des match Ausdrucks wird die auskommentierte Codezeile nicht kompiliert, aber die zweite Zeile zeigt, wie man das gewünschte List Objekt findet und diese Liste dann an die Variable x bindet. Wenn diese Codezeile mit einer Liste wie List(1,2,3) übereinstimmt, ergibt sie die Ausgabe List(1, 2, 3), wie in der Ausgabe der ersten Anweisung println zu sehen ist.

Das erste Beispiel Some zeigt, dass du mit dem gezeigten Ansatz einen Some abgleichen kannst, aber du kannst nicht auf seine Informationen auf der rechten Seite des Ausdrucks zugreifen. Das zweite Beispiel zeigt, wie du auf den Wert innerhalb des Some zugreifen kannst, und das dritte Beispiel geht noch einen Schritt weiter und gibt dir Zugriff auf das Some Objekt selbst. Der zweite Aufruf von println gibt Some(foo) aus und zeigt damit, dass du jetzt Zugriff auf das Objekt Some hast.

Schließlich wird dieser Ansatz verwendet, um eine Person zu finden, deren Nachname Doe ist. Mit dieser Syntax kannst du das Ergebnis der Musterübereinstimmung der Variablen p zuweisen und dann auf diese Variable auf der rechten Seite des Ausdrucks zugreifen.

Some und None in Match-Ausdrücken verwenden

Um diese Beispiele abzurunden, wirst du oft Some und None mit match Ausdrücken verwenden. Wenn du zum Beispiel mit einer Methode wie toIntOption versuchst, eine Zahl aus einer Zeichenkette zu erzeugen, kannst du das Ergebnis in einem match Ausdruck verarbeiten:

val s = "42"

// later in the code ...
s.toIntOption match
    case Some(i) => println(i)
    case None => println("That wasn't an Int")

Innerhalb des Ausdrucks match gibst du einfach die Fälle Some und None an, um die Erfolgs- und Fehlerbedingungen zu behandeln. Weitere Beispiele für die Verwendung von Option, Some und None findest du in Rezept 24.6, "Scalas Typen für die Fehlerbehandlung (Option, Tryund Either)".

Siehe auch

4.11 Verwendung von Enums und Case-Klassen in Match-Ausdrücken

Problem

Du willst Enums, Case-Klassen oder Case-Objekte in einem match Ausdruck abgleichen.

Lösung

Das folgende Beispiel zeigt, wie du Muster verwenden kannst, um Enums auf unterschiedliche Weise abzugleichen, je nachdem, welche Informationen du auf der rechten Seite jeder caseAnweisung brauchst. Zuerst haben wir hier eine enum mit dem Namen Animal, die drei Instanzen hat, Dog, Cat, undWoodpecker:

enum Animal:
    case Dog(name: String)
    case Cat(name: String)
    case Woodpecker

Ausgehend von enum zeigt diese getInfo Methode die verschiedenen Möglichkeiten, wie du die enum Typen in einem match Ausdruck abgleichen kannst:

import Animal.*

def getInfo(a: Animal): String = a match
    case Dog(moniker) => s"Got a Dog, name = $moniker"
    case _: Cat       => "Got a Cat (ignoring the name)"
    case Woodpecker   => "That was a Woodpecker"

Diese Beispiele zeigen, wie getInfo funktioniert, wenn Dog, Cat und Woodpecker angegeben werden:

println(getInfo(Dog("Fido")))     // Got a Dog, name = Fido
println(getInfo(Cat("Morris")))   // Got a Cat (ignoring the name)
println(getInfo(Woodpecker))      // That was a Woodpecker

Wenn die Klasse Dog in getInfo gefunden wird, wird ihr Name extrahiert und verwendet, um die Zeichenkette auf der rechten Seite des Ausdrucks zu erstellen. Um zu zeigen, dass der Variablenname, der beim Extrahieren des Namens verwendet wird, jeder legale Variablenname sein kann, verwende ich den Namen moniker.

Beim Abgleich mit Cat möchte ich den Namen ignorieren, also verwende ich die gezeigte Syntax, um jede Cat Instanz abzugleichen. Da Woodpecker nicht mit einem Parameter erstellt wird, wird es ebenfalls wie gezeigt abgeglichen.

Diskussion

In Scala 2 wurden versiegelte Traits mit Case-Klassen und Case-Objekten verwendet, um denselben Effekt wie mit enum zu erzielen:

sealed trait Animal
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal
case object Woodpecker extends Animal

Wie in Rezept 6.12, "Erstellen von Gruppen benannter Werte mit Enums", beschrieben , ist enum eine Abkürzung für die Definition (a) einer versiegelten Klasse oder eines Traits zusammen mit (b) Werten, die als Mitglieder des Begleitobjekts der Klasse definiert sind. Beide Ansätze können in dem match Ausdruck in getInfo verwendet werden, weil Case-Klassen eine eingebaute unapply Methode haben, mit der sie in match Ausdrücken funktionieren. Wie das funktioniert, beschreibe ich in Rezept 7.8, "Implementieren von Pattern Matching mit unapply".

4.12 Hinzufügen von if-Ausdrücken (Guards) zu Case-Anweisungen

Problem

Du möchtest einer case Anweisung in einem match Ausdruck eine einschränkende Logik hinzufügen, z.B. einen Zahlenbereich zulassen oder ein Muster abgleichen, aber nur, wenn dieses Muster einigen zusätzlichen Kriterien entspricht.

Lösung

Füge einen if guard zu deiner case Anweisung hinzu. Verwende sie, um einen Zahlenbereich abzugleichen:

i match
    case a if 0 to 9 contains a => println("0-9 range: " + a)
    case b if 10 to 19 contains b => println("10-19 range: " + b)
    case c if 20 to 29 contains c => println("20-29 range: " + c)
    case _ => println("Hmmm...")

Verwende sie, um verschiedene Werte eines Objekts abzugleichen:

i match
    case x if x == 1 => println("one, a lonely number")
    case x if (x == 2 || x == 3) => println(x)
    case _ => println("some other value")

Solange deine Klasse eine unapply Methode hat, kannst du in deinen if Guards auf Klassenfelder verweisen. Da zum Beispiel eine Case-Klasse eine automatisch generierte unapply Methode hat, können wir diese Stock Klasse und Instanz verwenden:

case class Stock(symbol: String, price: BigDecimal)
val stock = Stock("AAPL", BigDecimal(132.50))

kannst du Mustervergleiche und Schutzbedingungen mit den Klassenfeldern verwenden:

stock match
    case s if s.symbol == "AAPL" && s.price < 140 => buy(s)
    case s if s.symbol == "AAPL" && s.price > 160 => sell(s)
    case _ => // do nothing

Du kannst auch Felder aus case Klassen - und Klassen, die ordnungsgemäß implementierte unapply Methoden haben - extrahieren und diese in deinen Guard-Bedingungen verwenden. Zum Beispiel die case Anweisungen in diesem match Ausdruck:

// extract the 'name' in the 'case' and then use that value
def speak(p: Person): Unit = p match
    case Person(name) if name == "Fred" =>
        println("Yabba dabba doo")
    case Person(name) if name == "Bam Bam" =>
        println("Bam bam!")
    case _ =>
        println("Watch the Flintstones!")

funktioniert, wenn Person als Fallklasse definiert ist:

case class Person(aName: String)

oder als Klasse mit einer richtig implementierten unapply Methode:

class Person(val aName: String)
object Person:
    // 'unapply' deconstructs a Person. it’s also known as an
    // extractor, and Person is an “extractor object.”
    def unapply(p: Person): Option[String] = Some(p.aName)

In Rezept 7.8, "Implementieren von Pattern Matching mit unapply", findest du weitere Details, wie du unapply Methoden schreibst.

Diskussion

Solche if Ausdrücke kannst du immer dann verwenden, wenn du boolesche Tests auf der linken Seite von case Anweisungen (d.h. vor dem Symbol => ) hinzufügen möchtest.

Beachte, dass alle diese Beispiele geschrieben werden könnten, indem du die if Tests auf der rechten Seite der Ausdrücke einfügst, wie hier:

case Person(name) =>
    if name == "Fred" then println("Yabba dabba doo")
    else if name == "Bam Bam" then println("Bam bam!")

In vielen Situationen wird dein Code jedoch einfacher und leichter zu lesen sein, wenn du die if guard direkt mit der case Anweisung verbindest; dies hilft, die guard von der späteren Geschäftslogik zu trennen.

Beachte auch, dass dieses Person Beispiel ein wenig konstruiert ist, weil Scalas Pattern-Matching-Funktionen es dir erlauben, die Fälle so zu schreiben:

def speak(p: Person): Unit = p match
    case Person("Fred") => println("Yabba dabba doo")
    case Person("Bam Bam") => println("Bam bam!")
    case _ => println("Watch the Flintstones!")

In diesem Fall wird eine Guard wirklich benötigt, wenn Person komplexer ist und du etwas mehr tun musst, als nur die Parameter abzugleichen.

Außerdem kannst du, wie in Rezept 4.10 gezeigt, statt des in der Lösung gezeigten Codes

case x if (x == 2 || x == 3) => println(x)

Eine andere mögliche Lösung ist die Verwendung eines Variablen-Bindungsmusters:

case x @ (2|3) => println(x)

Dieser Code kann wie folgt gelesen werden: "Wenn der Wert des Ausdrucks match (i) 2 oder 3 ist, weise diesen Wert der Variablen x zu und drucke dann x mit println."

4.13 Einen Match-Ausdruck anstelle von isInstanceOf verwenden

Problem

Du willst einen Codeblock schreiben, der einem Typ oder mehreren verschiedenen Typen entspricht.

Lösung

Du kannst die Methode isInstanceOf verwenden, um den Typ eines Objekts zu testen:

if x.isInstanceOf[Foo] then ...

Der "Scala-Weg" ist jedoch, match Ausdrücke für diese Art von Arbeit zu bevorzugen, weil es im Allgemeinen viel mächtiger und bequemer ist, match als isInstanceOf zu verwenden.

In einem einfachen Anwendungsfall wird dir zum Beispiel ein Objekt unbekannten Typs gegeben und du möchtest feststellen, ob das Objekt eine Instanz von Person ist. Dieser Code zeigt, wie du einen match Ausdruck schreibst, der true zurückgibt, wenn der Typ Person ist, und andernfalls false:

def isPerson(m: Matchable): Boolean = m match
    case p: Person => true
    case _ => false

Ein häufigeres Szenario ist, dass du ein Modell wie dieses hast:

enum Shape:
    case Circle(radius: Double)
    case Square(length: Double)

und dann willst du eine Methode schreiben, um den Flächeninhalt von Shape zu berechnen. Eine Lösung für dieses Problem ist, area mit Hilfe von Mustervergleichen zu schreiben:

import Shape.*

def area(s: Shape): Double = s match
    case Circle(r) => Math.PI * r * r
    case Square(l) => l * l

// examples
area(Circle(2.0))   // 12.566370614359172
area(Square(2.0))   // 4.0

Dies ist eine häufige Anwendung, bei der area einen Parameter entgegennimmt, dessen Typ ein unmittelbarer Elternteil der Typen ist, die du in match dekonstruierst.

Wenn Circle und Square zusätzliche Konstruktorparameter hätten und du nur auf radius bzw. length zugreifen müsstest, sähe die vollständige Lösung so aus:

enum Shape:
    case Circle(x0: Double, y0: Double, radius: Double)
    case Square(x0: Double, y0: Double, length: Double)

import Shape.*

def area(s: Shape): Double = s match
    case Circle(_, _, r) => Math.PI * r * r
    case Square(_, _, l) => l * l

// examples
area(Circle(0, 0, 2.0))   // 12.566370614359172
area(Square(0, 0, 2.0))   // 4.0

Wie in den case Anweisungen innerhalb des match Ausdrucks gezeigt, ignorierst du einfach die Parameter, die du nicht brauchst, indem du auf sie mit dem _ Zeichen verweist.

Diskussion

Wie gezeigt, kannst du mit dem Ausdruck match mehrere Typen abgleichen. Die Verwendung dieses Ausdrucks als Ersatz für die Methode isInstanceOf ist also nur eine natürliche Anwendung der Syntax match/case und des allgemeinen Pattern-Matching-Ansatzes, der in Scala-Anwendungen verwendet wird.

Für die einfachsten Anwendungsfälle kann die Methode isInstanceOf eine einfachere Methode sein, um festzustellen, ob ein Objekt einem Typ entspricht:

if (o.isInstanceOf[Person]) { // handle this ...

Für alles, was komplexer ist als das, ist ein match Ausdruck besser lesbar als eine lange if/then/else if Anweisung.

Siehe auch

4.14 Arbeiten mit einer Liste in einem Match-Ausdruck

Problem

Du weißt, dass eine List Datenstruktur etwas anders ist als andere sequentielle Datenstrukturen: Sie wird aus Cons-Zellen aufgebaut und endet in einem Nil Element. Das willst du zu deinem Vorteil nutzen, wenn du mit einem match Ausdruck arbeitest, z. B. beim Schreiben einer rekursiven Funktion.

Lösung

Du kannst eine List erstellen, die die ganzen Zahlen 1, 2 und 3 enthält, wie folgt:

val xs = List(1, 2, 3)

oder so:

val ys = 1 :: 2 :: 3 :: Nil

Wie im zweiten Beispiel gezeigt, endet ein List mit einem Nil Element. Das kannst du dir zunutze machen, wenn du match Ausdrücke schreibst, um mit Listen zu arbeiten, insbesondere beim Schreiben rekursiver Algorithmen. In der folgenden listToString Methode zum Beispiel wird die Methode rekursiv mit dem Rest von List aufgerufen, wenn das aktuelle Element nicht Nil ist. Wenn das aktuelle Element jedoch Nil ist, werden die rekursiven Aufrufe gestoppt und ein leeres String zurückgegeben, woraufhin sich die rekursiven Aufrufe auflösen:

def listToString(list: List[String]): String = list match
    case s :: rest => s + " " + listToString(rest)
    case Nil => ""

Die REPL zeigt, wie diese Methode funktioniert:

scala> val fruits = "Apples" :: "Bananas" :: "Oranges" :: Nil
fruits: List[java.lang.String] = List(Apples, Bananas, Oranges)

scala> listToString(fruits)
res0: String = "Apples Bananas Oranges "

Derselbe Ansatz kann auch bei Listen anderer Typen und anderen Algorithmen verwendet werden. Während du zum Beispiel einfach List(1,2,3).sum schreiben könntest, zeigt dieses Beispiel, wie du deine eigene Summenmethode mit match und Rekursion schreiben kannst:

def sum(list: List[Int]): Int = list match
    case Nil => 0
    case n :: rest => n + sum(rest)

Auch dies ist ein Produktalgorithmus:

def product(list: List[Int]): Int = list match
    case Nil => 1
    case n :: rest => n * product(rest)

Die REPL zeigt, wie diese Methoden funktionieren:

scala> val nums = List(1,2,3,4,5)
nums: List[Int] = List(1, 2, 3, 4, 5)

scala> sum(nums)
res0: Int = 15

scala> product(nums)
res1: Int = 120

Reduzieren und Falten nicht vergessen

Rekursion ist zwar großartig, aber Scalas verschiedene Reduktions- und Faltungsmethoden in den Collections-Klassen ermöglichen es dir, eine Sammlung zu durchlaufen, während du einen Algorithmus anwendest, und machen eine Rekursion oft überflüssig. Du kannst zum Beispiel einen Summenalgorithmus mit reduce in einer dieser beiden Formen schreiben:

// long form
def sum(list: List[Int]): Int = list.reduce((x,y) => x + y)

// short form
def sum(list: List[Int]): Int = list.reduce(_ + _)

Weitere Informationen findest du in Rezept 13.10, "Mit den Methoden reduce und fold durch eine Sammlung gehen".

Diskussion

Wie gezeigt, ist die Rekursion eine Technik, bei der eine Methode sich selbst aufruft, um ein Problem zu lösen. In der funktionalen Programmierung - wo alle Variablen unveränderlich sind - bietet die Rekursion eine Möglichkeit, über die Elemente in einer List zu iterieren, um ein Problem zu lösen, z. B. die Summe oder das Produkt aller Elemente in einer List zu berechnen.

Das Schöne an der Arbeit mit der Klasse List ist, dass List mit dem Element Nil endet, sodass deine rekursiven Algorithmen typischerweise dieses Muster haben:

def myTraversalMethod[A](xs: List[A]): B = xs match
    case head :: tail =>
        // do something with the head
        // pass the tail of the list back to your method, i.e.,
        // `myTraversalMethod(tail)`
    case Nil =>
        // end condition here (0 for sum, 1 for product, etc.)
        // end the traversal

Variablen in der funktionalen Programmierung

In FP verwenden wir den Begriff Variablen, aber da wir nur unveränderliche Variablen verwenden, mag es scheinen, dass dieses Wort keinen Sinn macht, d.h. wir haben eine Variable, die sich nicht verändern kann.

Hier geht es darum, dass wir "Variable" im algebraischen Sinne meinen, nicht im Sinne der Computerprogrammierung. In der Algebra sagen wir zum Beispiel, dass a, b und c Variablen sind, wenn wir diese algebraische Gleichung schreiben:

a = b * c

Sobald sie jedoch zugewiesen sind, können sie nicht mehr verändert werden. Der Begriff Variable hat in der funktionalen Programmierung dieselbe Bedeutung.

Siehe auch

Anfangs fand ich, dass Rekursion ein unnötig schwer zu verstehendes Thema ist, deshalb habe ich schon einige Blogbeiträge darüber geschrieben:

4.15 Abgleichen einer oder mehrerer Ausnahmen mit try/catch

Problem

Du möchtest eine oder mehrere Ausnahmen in einem try/catch Block abfangen.

Lösung

Die Scala try/catch/finally Syntax ist ähnlich wie die von Java, aber sie verwendet den match Ausdrucksansatz im catch Block:

try
   doSomething()
catch
   case e: SomeException => e.printStackTrace
finally
   // do your cleanup work

Wenn du mehrere Ausnahmen abfangen und behandeln musst, füge die Ausnahmetypen einfach als verschiedene case Anweisungen hinzu:

try
   openAndReadAFile(filename)
catch
   case e: FileNotFoundException =>
       println(s"Couldn’t find $filename.")
   case e: IOException =>
       println(s"Had an IOException trying to read $filename.")

Du kannst den Code auch so schreiben, wenn du das möchtest:

try
    openAndReadAFile(filename)
catch
    case e: (FileNotFoundException | IOException) =>
        println(s"Had an IOException trying to read $filename")

Diskussion

Wie gezeigt, wird die Scala-Syntax case verwendet, um verschiedenen möglichen Ausnahmen zu entsprechen. Wenn du dich nicht darum kümmerst, welche bestimmten Ausnahmen ausgelöst werden, sondern sie alle abfangen und etwas mit ihnen machen willst, z. B. sie protokollieren, verwende diese Syntax:

try
   openAndReadAFile(filename)
catch
   case t: Throwable => logger.log(t)

Wenn du dich aus irgendeinem Grund nicht für den Wert der Ausnahme interessierst, kannst du sie auch alle abfangen und so ignorieren:

try
   openAndReadAFile(filename)
catch
   case _: Throwable => println("Nothing to worry about, just an exception")

Methoden, die auf try/catch basieren

Wie in der Einleitung dieses Kapitels gezeigt, kann ein try/catch/finally Block einen Wert zurückgeben und somit als Körper einer Methode verwendet werden. Die folgende Methode gibt ein Option[String] zurück. Sie gibt ein Some zurück, das ein String enthält, wenn die Datei gefunden wurde, und ein None, wenn es ein Problem beim Lesen der Datei gibt:

import scala.io.Source
import java.io.{FileNotFoundException, IOException}

def readFile(filename: String): Option[String] =
    try
        Some(Source.fromFile(filename).getLines.mkString)
    catch
        case _: (FileNotFoundException|IOException) => None

Dies zeigt eine Möglichkeit, einen Wert aus einem try Ausdruck zurückzugeben.

Heutzutage schreibe ich nur noch selten Methoden, die Ausnahmen auslösen, aber wie in Java kannst du eine Ausnahme über eine catch Klausel auslösen. Da es in Scala jedoch keine geprüften Ausnahmen gibt, musst du nicht angeben, dass eine Methode die Ausnahme auslöst. Das zeigt das folgende Beispiel, in dem die Methode in keiner Weise annotiert ist:

// danger: this method doesn’t warn you that an exception can be thrown
def readFile(filename: String): String =
    try
        Source.fromFile(filename).getLines.mkString
    catch
        case t: Throwable => throw t

Das ist eine schrecklich gefährliche Methode - schreibe nicht so einen Code!

Um zu erklären, dass eine Methode eine Ausnahme auslöst, fügst du die Annotation @throws zu deiner Methodendefinition hinzu:

// better: this method warns others that an exception can be thrown
@throws(classOf[NumberFormatException])
def readFile(filename: String): String =
    try
        Source.fromFile(filename).getLines.mkString
    catch
        case t: Throwable => throw t

Diese letzte Methode ist zwar besser als die vorherige, aber keine von beiden ist zu bevorzugen. Der "Scala-Weg" ist, niemals Ausnahmen zu werfen. Stattdessen solltest du Option verwenden, wie zuvor gezeigt, oder die Klassen Try/Success/Failure oder Either/Right/Left benutzen, wenn du Informationen darüber zurückgeben willst, was fehlgeschlagen ist. Dieses Beispiel zeigt, wie du Try verwenden kannst:

import scala.io.Source
import java.io.{FileNotFoundException, IOException}
import scala.util.{Try,Success,Failure}

def readFile(filename: String): Try[String] =
    try
        Success(Source.fromFile(filename).getLines.mkString)
    catch
        case t: Throwable => Failure(t)

Wenn es um eine Ausnahmemeldung geht, bevorzuge ich immer Try oder Either anstelle von Option, weil du damit Zugriff auf die Meldung in Failure oder Left hast, während Option nur None zurückgibt.

Eine übersichtliche Art, alles zu erfassen

Eine weitere prägnante Möglichkeit, alle Ausnahmen abzufangen, ist die Methode allCatch des scala.util.control.Exception Objekts. Die folgenden Beispiele zeigen, wie allCatch verwendet werden kann, und zwar zuerst im Erfolgsfall und dann im Fehlerfall. Die Ausgabe der einzelnen Ausdrücke wird nach dem Kommentar in jeder Zeile angezeigt:

import scala.util.control.Exception.allCatch

// OPTION
allCatch.opt("42".toInt)      // Option[Int] = Some(42)
allCatch.opt("foo".toInt)     // Option[Int] = None

// TRY
allCatch.toTry("42".toInt)    // Matchable = 42
allCatch.toTry("foo".toInt)
   // Matchable = Failure(NumberFormatException: For input string: "foo")

// EITHER
allCatch.either("42".toInt)   // Either[Throwable, Int] = Right(42)
allCatch.either("foo".toInt)
   // Either[Throwable, Int] =
   // Left(NumberFormatException: For input string: "foo")

Siehe auch

4.16 Eine Variable deklarieren, bevor sie in einemtry/catch/finally-Block verwendet wird

Problem

Du willst ein Objekt in einem try Block verwenden und musst im finally Teil des Blocks auf das Objekt zugreifen, z.B. wenn du eine close Methode für ein Objekt aufrufen musst.

Lösung

Im Allgemeinen deklarierst du dein Feld als Option vor dem try/catch Block und bindest die Variable dann an eine Some in der try Klausel. Das folgende Beispiel zeigt, dass das Feld sourceOption vor dem try/catch Block deklariert und innerhalb der try Klausel zugewiesen wird:

import scala.io.Source
import java.io.*

var sourceOption: Option[Source] = None
try
    sourceOption = Some(Source.fromFile("/etc/passwd"))
    sourceOption.foreach { source =>
        // do whatever you need to do with 'source' here ...
        for line <- source.getLines do println(line.toUpperCase)
    }
catch
    case ioe: IOException => ioe.printStackTrace
    case fnf: FileNotFoundException => fnf.printStackTrace
finally
    sourceOption match
        case None =>
            println("bufferedSource == None")
        case Some(s) =>
            println("closing the bufferedSource ...")
            s.close

Dies ist ein erfundenes Beispiel - und Rezept 16.1, "Lesen von Textdateien", zeigt einen viel besseren Weg, Dateien zu lesen - aber es zeigt die Vorgehensweise. Zuerst definierst du ein var Feld als Option vor dem try Block:

var sourceOption: Option[Source] = None

Weisen Sie dann innerhalb der try Klausel der Variablen einen Some Wert zu:

sourceOption = Some(Source.fromFile("/etc/passwd"))

Wenn du eine Ressource schließen musst, verwende eine Technik wie die hier gezeigte (obwohl Rezept 16.1, "Lesen von Textdateien", auch eine viel bessere Methode zum Schließen von Ressourcen zeigt). Wenn in diesem Code eine Ausnahme ausgelöst wird, wird sourceOption innerhalb von finally zu einem None Wert. Wenn keine Ausnahme ausgelöst wird, wird der Some Zweig des match Ausdrucks ausgewertet.

Diskussion

Ein Schlüssel zu diesem Rezept ist die Kenntnis der Syntax für die Deklaration von Option Feldern, die anfangs nicht ausgefüllt sind:

var in: Option[FileInputStream] = None
var out: Option[FileOutputStream] = None

Diese zweite Form kann auch verwendet werden, aber die erste Form wird bevorzugt:

var in = None: Option[FileInputStream]
var out = None: Option[FileOutputStream]

Nicht null verwenden

Als ich anfing, mit Scala zu arbeiten, war die einzige Möglichkeit, diesen Code zu schreiben, die Verwendung von null Werten. Der folgende Code demonstriert den Ansatz, den ich in einer Anwendung verwendet habe, die meine E-Mail-Konten überprüft. Die Felder store und inbox in diesem Code sind als null Felder deklariert, die die Typen Store und Folder (aus dem javax.mail Paket) haben:

// (1) declare the null variables (don’t use null; this is just an example)
var store: Store = null
var inbox: Folder = null

try
    // (2) use the variables/fields in the try block
    store = session.getStore("imaps")
    inbox = getFolder(store, "INBOX")
    // rest of the code here ...
catch
    case e: NoSuchProviderException => e.printStackTrace
    case me: MessagingException => me.printStackTrace
finally
    // (3) call close() on the objects in the finally clause
    if (inbox != null) inbox.close
    if (store != null) store.close

Wenn du in Scala arbeitest, kannst du jedoch vergessen, dass es null überhaupt gibt, daher ist diese Vorgehensweise nicht empfehlenswert.

Siehe auch

In diesen Rezepten findest du weitere Informationen darüber, (a) wie du die Werte von null nicht verwenden kannst und (b) wie du stattdessen Option, Try und Either verwenden kannst:

Wenn du Code schreibst, der eine Ressource beim Start öffnen und beim Beenden schließen muss, kann es hilfreich sein, das Objektscala.util.Using zu verwenden. In Rezept 16.1, "Lesen von Textdateien", findest du ein Beispiel für die Verwendung dieses Objekts und eine viel bessere Methode zum Lesen einer Textdatei.

Rezept 24.8, "Behandlung von Optionswerten mit Funktionen höherer Ordnung", zeigt auch andere Möglichkeiten, mit Option Werten zu arbeiten, als einen match Ausdruck zu verwenden.

4.17 Eigene Kontrollstrukturen erstellen

Problem

Du möchtest deine eigenen Kontrollstrukturen definieren, um die Scala-Sprache anzupassen, deinen Code zu vereinfachen oder eine domänenspezifische Sprache (DSL) zu erstellen.

Lösung

Dank Funktionen wie mehreren Parameterlisten, By-Name-Parametern, Erweiterungsmethoden, Funktionen höherer Ordnung und mehr kannst du deinen eigenen Code erstellen, der genau wie eine Kontrollstruktur funktioniert.

Stell dir zum Beispiel vor, dass Scala keine eingebaute while Schleife hat und du deine eigene whileTrue Schleife erstellen möchtest, die du wie folgt verwenden kannst:

var i = 0
whileTrue (i < 5) {
    println(i)
    i += 1
}

Um diese whileTrue Kontrollstruktur zu erstellen, definierst du eine Methode namens whileTrue, die zwei Parameterlisten entgegennimmt. Die erste Parameterliste enthält die Testbedingung - in diesem Fall i < 5- und die zweite Parameterliste ist der Codeblock, den der Benutzer ausführen möchte, d. h. der Code zwischen den geschweiften Klammern. Definiere beide Parameter als By-Name-Parameter. Da whileTrue nur für Seiteneffekte verwendet wird, wie z. B. die Aktualisierung veränderbarer Variablen oder die Ausgabe auf der Konsole, gibst du an, dass Unit zurückgegeben wird. Eine erste Skizze der Methodensignatur sieht wie folgt aus:

def whileTrue(testCondition: => Boolean)(codeBlock: => Unit): Unit = ???

Eine Möglichkeit, den Hauptteil der Methode zu implementieren, besteht darin, einen rekursiven Algorithmus zu schreiben. Dieser Code zeigt eine vollständige Lösung:

import scala.annotation.tailrec

object WhileTrue:
    @tailrec
    def whileTrue(testCondition: => Boolean)(codeBlock: => Unit): Unit =
        if (testCondition) then
            codeBlock
            whileTrue(testCondition)(codeBlock)
        end if
    end whileTrue

In diesem Code wird testCondition ausgewertet, und wenn die Bedingung wahr ist, wird codeBlock ausgeführt, und dann wird whileTrue rekursiv aufgerufen. Er ruft sich so lange selbst auf, bis testCondition false zurückgibt.

Um diesen Code zu testen, importiere ihn zunächst:

import WhileTrue.whileTrue

Führe dann die zuvor gezeigte whileTrue Schleife aus und du wirst sehen, dass sie wie gewünscht funktioniert.

Diskussion

Die Schöpfer der Sprache Scala haben sich bewusst dafür entschieden, einige Schlüsselwörter nicht in Scala zu implementieren und stattdessen Funktionen durch Scala-Bibliotheken zu implementieren. Scala hat zum Beispiel keine eingebauten Schlüsselwörter break und continue. Stattdessen werden sie durch eine Bibliothek implementiert, wie ich in meinem Blogbeitrag "Scala: How to Use break and continue in for and while Loops" beschreibe.

Wie in der Lösung gezeigt, kannst du mit Funktionen wie diesen deine eigenen Kontrollstrukturen erstellen:

  • Mit mehreren Parameterlisten kannst du das tun, was ich mit whileTrue gemacht habe: eine Parametergruppe für die Testbedingung und eine zweite Gruppe für den Codeblock erstellen.

  • Mit By-Name-Parametern kannst du auch das tun, was ich mit whileTrue gemacht habe: Parameter akzeptieren, die erst ausgewertet werden, wenn sie in deiner Methode aufgerufen werden.

Auch andere Funktionen wie die Infix-Notation, Funktionen höherer Ordnung, Erweiterungsmethoden und fließende Schnittstellen ermöglichen es dir, andere benutzerdefinierte Kontrollstrukturen und DSLs zu erstellen.

By-name Parameter

By-name-Parameter sind ein wichtiger Teil der whileTrue Kontrollstruktur. In Scala ist es wichtig zu wissen, dass, wenn du Methodenparameter mit der => Syntax definierst:

def whileTrue(testCondition: => Boolean)(codeBlock: => Unit) =
                           -----                  -----

erstellst du einen so genannten Call-by-Name- oder By-Name-Parameter. Ein By-Name-Parameter wird nur ausgewertet, wenn auf ihn innerhalb deiner Methode zugegriffen wird. Wie ich in meinen Blog-Beiträgen "Wie man By-Name-Parameter in Scala verwendet" und "Scala und Call-By-Name-Parameter" schreibe , ist der genauere Name für diese Parameter " evaluate when accessed". Denn genau so funktionieren sie: Sie werden nur ausgewertet, wenn auf sie innerhalb deiner Methode zugegriffen wird. Wie ich in diesem zweiten Blogbeitrag schreibe, vergleicht Rob Norris einen By-Name-Parameter mit einer def Methode.

Ein weiteres Beispiel

Im Beispiel whileTrue habe ich einen rekursiven Aufruf verwendet, um die Schleife am Laufen zu halten, aber für einfachere Kontrollstrukturen brauchst du keine Rekursion. Nimm zum Beispiel an, dass du eine Kontrollstruktur brauchst, die zwei Testbedingungen akzeptiert. Wenn beide den Wert true ergeben, wird ein Codeblock ausgeführt, der mitgeliefert wird. Ein Ausdruck, der diese Kontrollstruktur verwendet, sieht wie folgt aus:

doubleIf(age > 18)(numAccidents == 0) { println("Discount!") }

In diesem Fall definierst du doubleIf als Methode, die drei Parameterlisten entgegennimmt, wobei jeder Parameter wiederum ein By-Name-Parameter ist:

// two 'if' condition tests
def doubleIf(test1: => Boolean)(test2: => Boolean)(codeBlock: => Unit) =
    if test1 && test2 then codeBlock

Da doubleIf nur einen Test durchführen und keine Endlosschleife bilden muss, ist kein rekursiver Aufruf im Methodenkörper erforderlich. Sie prüft einfach die beiden Testbedingungen, und wenn sie true ergeben, wird codeBlock ausgeführt.

Siehe auch

  • Eine meiner Lieblingsanwendungen dieser Technik wird in dem Buch Beginning Scala von David Pollak (Apress) gezeigt. Obwohl sie durch das scala.util.Using Objekt überflüssig geworden ist, beschreibe ich in diesem Blogbeitrag "The using Control Structure in Beginning Scala", wie diese Technik funktioniert .

  • Die Scala-Klasse Breaks wird verwendet, um Break- und Continue-Funktionen in for -Schleifen zu implementieren, und ich habe darüber geschrieben: "Scala: How to Use break and continue in for and while Loops". Der Quellcode der Klasse Breaks ist recht einfach und bietet ein weiteres Beispiel für die Implementierung einer Kontrollstruktur. Du findest ihren Quellcode als Link auf ihrer Scaladoc-Seite.

Get Scala Kochbuch, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.