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 undfor
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 if
Anweisungen:
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
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
-
Weitere Beispiele für die Verwendung von
zipWithIndex
findest du in Rezept 13.4, "Verwenden von zipWithIndex oder zip zum Erstellen von Schleifenzählern". -
Weitere Beispiele dafür, wie du über die Elemente in einer
Map
iterieren kannst, findest du in Rezept 14.9, "Traversieren einer Karte".
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
(
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:
-
Zu Beginn der Schleife
for
/yield
wird sofort eine neue leere Sammlung erstellt, die denselben Typ hat wie die Eingabesammlung. Wenn der Eingabetyp zum BeispielVector
ist, ist der Ausgabetyp auchVector
. Du kannst dir diese neue Sammlung wie einen leeren Eimer vorstellen. -
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. -
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
-
Vergleiche zwischen
for
comprehensions undmap
werden in Rezept 13.5, "Umwandlung einer Sammlung in eine andere mit einer Map", ausführlicher dargestellt .
4.5 Das if-Konstrukt wie einen ternären Operator verwenden
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
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
-
Weitere Informationen darüber, wie JVM-Switches funktionieren, findest du in der JVM-Spezifikation zum Kompilieren von Switches.
-
Zum Unterschied zwischen
lookupswitch
undtableswitch
heißt es auf dieser Stack Overflow-Seite: "Der Unterschied besteht darin, dass ein Lookupswitch eine Tabelle mit Schlüsseln und Bezeichnungen verwendet, während ein Tableswitch nur eine Tabelle mit Bezeichnungen verwendet." Weitere Informationen findest du im Abschnitt "Compiling Switches" in der Spezifikation der Java Virtual Machine (JVM). -
Weitere Informationen zu Teilfunktionen findest du in Rezept 10.7, "Erstellen von Teilfunktionen".
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
-
Siehe Rezept 4.12 für einen ähnlichen Ansatz.
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
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 einInt
Wert von0
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 dercase
Anweisung verwenden kannst. Zum Beispiel kannst du am Ende einesmatch
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 undstr
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 anvariableName
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 Element1
ist, aber bisher wurde auf der rechten Seite des Ausdrucks nicht auf dieList
zugegriffen. Wenn du auf eineList
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 desmatch
Ausdrucks wird die auskommentierte Codezeile nicht kompiliert, aber die zweite Zeile zeigt, wie man das gewünschteList
Objekt findet und diese Liste dann an die Variablex
bindet. Wenn diese Codezeile mit einer Liste wieList(1,2,3)
übereinstimmt, ergibt sie die AusgabeList(1, 2, 3)
, wie in der Ausgabe der ersten Anweisungprintln
zu sehen ist.Das erste Beispiel
Some
zeigt, dass du mit dem gezeigten Ansatz einenSome
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 desSome
zugreifen kannst, und das dritte Beispiel geht noch einen Schritt weiter und gibt dir Zugriff auf dasSome
Objekt selbst. Der zweite Aufruf vonprintln
gibtSome(foo)
aus und zeigt damit, dass du jetzt Zugriff auf das ObjektSome
hast.Schließlich wird dieser Ansatz verwendet, um eine
Person
zu finden, deren NachnameDoe
ist. Mit dieser Syntax kannst du das Ergebnis der Musterübereinstimmung der Variablenp
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
-
Eine Diskussion über die Umgehung von Typ-Löschung bei der Verwendung von
match
Ausdrücken auf Stack Overflow
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 case
Anweisung 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
-
Rezept 4.10 zeigt viele weitere
match
Techniken.
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:
-
In "Rekursion: Rekursives Denken" schreibe ich über Identitätselemente, z. B. dass
0
ein Identitätselement für die Summenoperation ist,1
ein Identitätselement für die Produktoperation und""
(eine leere Zeichenkette) ein Identitätselement für die Arbeit mit Zeichenketten ist.
4.15 Abgleichen einer oder mehrerer Ausnahmen mit try/catch
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
-
In Rezept 8.7, "Deklaration, dass eine Methode eine Ausnahme werfen kann", findest du weitere Beispiele für die Deklaration, dass eine Methode eine Ausnahme werfen kann.
-
Siehe Rezept 24.6, "Verwendung der Fehlerbehandlungstypen von Scala (Option, Tryund Either)", für weitere Informationen zur Verwendung von
Option
/Some
/None
undTry
/Success
/Failure
. -
Auf der Seite
scala.util.control.Exception
Scaladoc findest du weitereInformationenallCatch
.
4.16 Eine Variable deklarieren, bevor sie in einemtry/catch/finally-Block verwendet wird
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:
-
Rezept 24.6, "Scalas Typen für die Fehlerbehandlung (Option, Tryund Either)verwenden"
-
Rezept 24.8, "Behandlung von Optionswerten mit Funktionen höherer Ordnung"
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
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 infor
-Schleifen zu implementieren, und ich habe darüber geschrieben: "Scala: How to Use break and continue in for and while Loops". Der Quellcode der KlasseBreaks
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.