4. 控制结构
正如名字所言,控制结构给程序员提供了一种控制程序执行流的方式。这是一个程序语言进行决策和轮询任务的基本功能。回到学习Scala的2010年,我曾认为控制结构如if/then语句,以及for和while循环,是一个相对无聊的程序功能,但那只是因为我并不知道还有其他方式。这些天我知道了控制结构一个程序语言定义性的功能(defining feature)。
Scala的控制结构包括:
for循环和for表达式
if/then/else语句和if表达式
match 表达式 (模式匹配)
try/catch/finally块
while循环
接下来我会简单介绍一下,之后会在具体的小节中展示更多的细节来描述如何使用这些功能。
for循环和for表达式
在大多数的基础用法中, for循环提供了一个针对集合的迭代操作,如下:
for i <- List(1, 2, 3) do println(i)
但是这仅仅只是一个基础用法。for循环也可以包含守卫 —— 嵌入if语句:
for
i <- 1 to 10
if i > 3
if i < 6
do
println(i)
通过使用yield关键字,for循环也变成了 for 表达式 —— 产生结果的循环:
val listOfInts = for
i <- 1 to 10
if i > 3
if i < 6
yield
i * 10
在循环运行后,listOfInts变成了一个Vector(40, 50)。这个在循环中的守卫语句过滤掉了除了4和5的所有值,并将这些值在yield代码块中乘以10。
关于for循环和表达式的更多细节将在本章的初始章节中涉及。
if/then/else-if表达式
正如for循环和表达式给你遍历集合的能力,if/then/else 表达式则提供了分支决策的能力。在Scala 3中,首选的语法已经改变了,现在看起来是这样的:
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
正如两个例子中的展示,一个if表达式就是一个带返回值的表达式(表达式的讨论参阅4.5小节)。
match表达式和模式匹配
接下来,match表达式和模式匹配是Scala的基本功能,本章节的大部分内容将展示这些功能。正如if表达式,match表达式也有返回值,所以可以把他们用作方法体。
比如下面的一个例子,这个方法相似于Perl语言版的 true 或 false 判断:
def isTrue(a: Matchable): Boolean = a match
case false | 0 | "" => false
case _ => true
这段代码中, 如果isTrue收到了0或者空字串,它将返回false,否者true。本章花了十个小节来描述match表达式的功能细节。
try/catch/finally代码块
Scala的try/catch/finally代码块类似于Java,但语法上有一点不同,catch代码块是由match表达式构成的:
try
// 一些异常构造代码
catch
case e1: Exception1Type => // 处理该类异常
case e2: Exception2Type => // 处理该类异常
finally
// 关闭资源和执行其他行文
正如if和match,try也是一个会返回值的表达式,你可以如下进行String到Int的转换:
def toInt(s: String): Option[Int] =
try
Some(s.toInt)
catch
case e: NumberFormatException => None
这些例子展现了toInt如何运行:
toInt("1") // Option[Int] = Some(1)
toInt("Yo") // Option[Int] = None
关于 try/catch 代码块的更多信息请参阅4.16小节。
while循环
当来到while循环,你会发现它是真的很少有在Scala中用到。这是因为while循环几乎都是用于副作用,比如更改可变参数和用println打印,并且这些都是可以通过for循环和foreach方法对集合进行操作的。也就是说,如果你需要用到它,大体如下所示:
while
i < 10
do
println(i)
i += 1
4.1小节将简要介绍 while 循环。
最后,通过数个Scala功能的合并,你可以创建自己的控制结构,这些将在4.17小节进行讨论。
控制结构是编程语言的界定特征
在2020年末,我足够幸运的合写了在Scala官方文档网站( https://docs.scala-lang.org/ )的Scala 3 Book,包括如下三章:
给Java开发者的Scala ( https://oreil.ly/Psbc4 )
给JS开发者的Scala ( https://oreil.ly/cDyzW )
给Python开发者的Scala( https://oreil.ly/6zIAX )
当我之前说控制结构作为“编程语言的界定特征”时,我的意思之一是,在我写完这些章节后,我开始意识到该章节所述功能的威力,以及和其他语言相比Scala的一致性是如何强大。这样的一致性是保持Scala能被愉快使用的功能之一。
4.1 在数据结构上的for循环
问题
你想要用一个传统for循环的方式来遍历一个集合的所有元素。
解决方案
有很多方法可以遍历 Scala 集合,包括 for 循环、while 循环、以及如foreach、map、flatMap 等集合方法。 该解决方案重点主要在for循环上。
给定一个简单的列表:
val fruits = List("apple", "banana", "orange")
你可以像这样遍历这些元素并打印它们:
scala> for f <- fruits do println(f)
apple
banana
orange
同样的方法适用于所有序列,包括 List、Seq、Vector、Array、ArrayBuffer等。
当你的算法需要多行时,使用相同的 for 循环语法,并将你的处理程序放在大括号内的一个块中:
scala> for f <- fruits do
| // imagine this requires multiple lines
| val s = f.toUpperCase
| println(s)
APPLE
BANANA
ORANGE
for循环计数器
如果你需要访问 for 循环内的计数器,请使用以下方法之一。
首先,你可以使用这样的计数器访问序列中的元素:
for i <- 0 until fruits.length do
println(s"$i is ${fruits(i)}")
该循环产生此输出:
0 is apple
1 is banana
2 is orange
你很少需要依靠索引访问序列元素,然而当你需要时,这是一种可能的方法。Scala集合还提供了一个 zipWithIndex 方法,可用于创建循环计数器:
for (fruit, index) <- fruits.zipWithIndex do
println(s"$index is $fruit")
它的输出如下:
0 is apple
1 is banana
2 is orange
生成器
在相关记录中,以下示例展示了如何使用 Range 执行循环三次:
scala> for i <- 1 to 3 do println(i)
1
2
3
for循环中的 1 to 3 代码部分创建了一个 Range,如REPL中所示:
scala> 1 to 3
res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3)
像这样使用一个 Range 对象,被称为使用一个 生成器(generator)。4.2小节展示了如何使用此技术创建带有多个计数器的for循环。
Map上的循环
当迭代 Map 中的键和值时,我发现这样的for循环最简洁易读的:
val names = Map(
"firstName" -> "Robert",
"lastName" -> "Goren"
)
for (k,v) <- names do println(s"key: $k, value: $v")
REPL展示了的输出是:
scala> for (k,v) <- names do println(s"key: $k, value: $v")
key: firstName, value: Robert
key: lastName, value: Goren
讨论
因为我已经切换到函数式编程风格,所以我已经好几年没有使用while循环了,但是REPL展示了它是如何工作的:
scala> var i = 0
i: Int = 0
scala> while i < 3 do
| println(i)
| i += 1
0
1
2
while 循环通常用于副作用,例如,像我这样更新可变变量以及向外界输出。随着我的代码越来越接近纯函数式编程 —— 一种没有可变状态的方式 —— 我不再需要 while。
也就是说,当你以面向对象的编程风格进行编程时,仍然会经常使用while循环,该示例展示了它们的语法。 也可以像这样把一个while循环写在多行上:
while
i < 10
do
println(i)
i += 1
集合方法,比如foreach
在某些方面,Scala 让我想起了 Perl 的口号,“有不止一种方法可以做它”,并且集合上的迭代提供了一些很好的例子。随着集合上可用的大量方法,这重点声明的是一个 for 循环甚至可能不是解决特定问题的最佳方法;foreach、map、flatMap、collect、reduce 等方法经常可以用来解决你的问题而不需要显式的 for 循环。
例如,当你使用集合时,你还可以通过调用集合上的foreach方法遍历每个集合元素:
scala> fruits.foreach(println)
apple
banana
orange
当你有一个算法要在集合中的每个元素上运行时,只需将这个匿名函数传递给foreach:
scala> fruits.foreach(e => println(e.toUpperCase))
APPLE
BANANA
ORANGE
与for循环一样,如果你的算法需要多行,请在一个代码块中执行相应的程序:
scala> fruits.foreach { e =>
| val s = e.toUpperCase
| println(s)
| }
APPLE
BANANA
ORANGE
另见
有关如何使用zipWithIndex的更多示例,请参阅13.4小节“使用zipWithIndex或zip以创建循环计数器”。
有关如何迭代Map中的元素的更多示例,请参阅14.9小节“遍历Map”。
for 循环的工作原理非常有趣,了解它可以帮助你进步。我在这些文章中详细介绍了这一点:
“How to Make a Custom Sequence Work as a for Loop Generator” ( https://oreil.ly/UFVQ2 )
“How to Enable Filtering in a for Expression” ( https://oreil.ly/3anl9 )
“How to Enable the Use of Multiple Generators in a for Expression”( https://oreil.ly/RZGNP )
4.2 使用带有多个计数器的for循环
问题
你想创建一个具有多个计数器的循环,例如在迭代多维数组时。
解决方案
你可以创建如下带有两个计数器的for循环:
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
执行如下:i 被设置为 1 时,遍历 j 中的元素,然后将 i 设置为 2 并重复该过程。
使用这种方法可以很好地处理这些简单示例,但是当你的代码变复杂时,这是首选样式:
for
i <- 1 to 3
j <- 1 to 5
k <- 1 to 10 by 2
do
println(s"i = $i, j = $j, k = $k")
这种方法在循环多维数组时很有用。假设你创建并填充一个像这样的小型二维数组:
val a = Array.ofDim[Int](2,2)
a(0)(0) = 0
a(0)(1) = 1
a(1)(0) = 2
a(1)(1) = 3
你可以像这样打印每一个数组元素:
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
讨论
如15.2小节“创建Range”所示,1 to 5 的语法创建了一个Range:
scala> 1 to 5
val res0: scala.collection.immutable.Range.Inclusive = Range 1 to 5
Range适用于许多目的,并且在for循环中使用的 <- 符号创建的Range称为生成器。同理,你也可以轻松在一个循环中使用多个生成器。
另见
关于格式化控制结构的Scala风格指南( https://oreil.ly/3jB28 )
4.3 使用带有嵌入式if语句的for循环(守卫语句)
问题
你希望在for循环中添加一个或多个条件子句,通常是为了过滤掉集合中的某些元素,同时处理其他元素。
解决方案
在生成器之后添加一个或多个if语句,如下所示:
for
i <- 1 to 10
if i % 2 == 0
do
print(s"$i ")
// output: 2 4 6 8 10
这些if语句被称为filter,filter表达式或守卫语句,而你可以根据手头的问题使用尽可能需要的守卫语句。这个循环显示了一个打印数字4的生硬方式:
for
i <- 1 to 10
if i > 3
if i < 6
if i % 2 == 0
do
println(i)
讨论
使用旧样式的if表达式编写for循环仍然是可以的。例如,给定这段代码:
import java.io.File
val dir = File(".")
val files: Array[java.io.File] = dir.listFiles()
理论上,你可以像这样用这种风格编写一个 for 循环,这让人想起C和Java:
// 一个C/Java风格的'for'循环
for (file <- files) {
if (file.isFile && file.getName.endsWith(".scala")) {
println(s"Scala file: $file")
}
}
然而,一旦你熟悉了Scala的for循环语法,我想你会发现它使代码更具可读性,因为它将循环和过滤的关注点从业务逻辑中分开:
for
// 循环和过滤
file <- files
if file.isFile
if file.getName.endsWith(".scala")
do
// 所需要的业务代码
println(s"Scala file: $file")
请注意,由于守卫语句通常旨在过滤集合,因此你可能希望使用可用于集合的众多过滤方法中的一种(filter、take、drop等),而不是for循环,具体取决于你的需求。具体例子请参阅第11章。
4.4 通过for/yield从现有集合创建新集合
问题
你想从一个现有的集合通过应用一个算法(和可能的一个或多个守卫语句)到每个元素来创建一个新的集合。
解决方案
使用一个带有for循环的yield语句来从一个现有的集合中创建一个新的集合。例如,给定一个小写的字符串数组:
scala> val names = List("chris", "ed", "maurice")
val names: List[String] = List(chris, ed, maurice)
你可以通过将带有for循环的yield和一个简单的算法结合起来新创建一个首字母大写的字符串数组:
scala> val capNames = for name <- names yield name.capitalize
val capNames: List[String] = List(Chris, Ed, Maurice)
使用带有yield语句的for循环称为for 推断(for-comprehension)。
如果你的算法需要多行代码,请在块中通过yield关键字,并手动指定结果变量的类型来实现,或者不用:
// [1] 声明 `lengths` 的类型
val lengths: List[Int] = for name <- names yield
// 假设这个主体需要多行代码
name.length
// [2] 不用声明 `lengths` 的类型
val lengths = for name <- names yield
// 假设这个主体需要多行代码
name.length
两种方法yield可以得到相同的结果:
List[Int] = List(5, 2, 7)
for推断(也称为for表达式)的两个部分都可以变得比较复杂。下面是一个更大的例子:
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
for推断产生以下结果:
a: List[Int] = List(49, 56, 63, 56, 64, 63)
一个for推断甚至可以是完整方法体:
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)
讨论
如果你不熟悉将yield与for循环一起使用,则可以这样考虑循环:
当它开始运行时,for/yield循环立即创建一个新的与输入集合具有相同类型的空集合。例如,如果输入类型是Vector,输出类型也将是Vector。你可以想象这个新集合就像一个空桶。
在for循环的每次迭代中,可能会从输入集合创建一个新的输出元素。输出元素被创建时,它就会被放在桶里。
当循环结束运行时,返回桶的全部内容。
这值是一种简化的描述,但我发现它在解释过程时很有帮助。
注意,写一个不带守卫的for表达式就像在一个集合上调用map方法一样。
例如,下面的for推断将fruits中的所有字符串集合转为大写:
scala> val namesUpper = for n <- names yield n.toUpperCase
val namesUpper: List[String] = List(CHRIS, ED, MAURICE)
在集合上调用map方法做同样的事情:
scala> val namesUpper = names.map(_.toUpperCase)
val namesUpper: List[String] = List(CHRIS, ED, MAURICE)
当我刚开始学习Scala时,我所有的代码都是用 for/yield 表达式写的,直到有一天我意识到,使用不带守卫的 for/yield ,就等于使用 map方法。
另见
13.5小节“使用map将一个集合转换为另一个集合”,更详细了展示了for推断和map方法之间的比较。
4.5 像三元运算符一样使用if结构
问题
你熟悉如下Java中特殊的 三元运算符(ternary operator或称三目运算符 ) 语法:
int absValue = (a < 0) ? -a : a;
并且你想知道如何在Scala中使用等价的表达方式。
解决方案
这是一个有点棘手的问题,因为与 Java 不同的是,在 Scala 中没有特殊的三元操作符;所以只需使用 if/else/then 表达式:
val a = 1
val absValue = if a < 0 then -a else a
因为 if 表达式返回一个值,所以你可以将其嵌入到 print 语句中:
println(if a == 0 then "a" else "b")
你还可以在另一个表达式中使用它,例如 hashCode 方法的这一部分:
hash = hash * prime + (if name == null then 0 else name.hashCode)
if/else 表达式返回值这一事实也让你可以编写简洁的方法:
// 版本 1:单行样式
def abs(x: Int) = if x >= 0 then x else -x
def max(a: Int, b: Int) = if a > b then a else b
// 版本 2:方法体在单独的一行,如果你愿意的话
def abs(x: Int) =
if x >= 0 then x else -x
def max(a: Int, b: Int) =
if a > b then a else b
讨论
“等式、关系和条件运算符”的Java文档页面( https://oreil.ly/rUDNP )指出,Java条件运算符 ?: “被称为三元运算符,因为它使用三个操作数。”
Java 在这里需要单独的语法,因为Java的if/else构造是一个语句;它没有返回值,仅用于副作用,例如更新可变字段。相反,因为Scala的if/else/then确实是一个表达式,并不需要特殊的操作符。参阅 24.3小节“编写表达式(而不是语句)”,以获取更多的语句和表达式的细节。
Arity(操作符词量) -- TODO 鸽子栏
三元 这个词与函数的操作符数量( Arity )有关。维基百科的“Arity页面”( https://oreil.ly/CJtbV )指出,“在逻辑、数学和计算机科学中,函数或操作的操作符数量是函数采用的参数或操作数的数量。” 一元运算符采用一个操作数,二元运算符采用两个操作数,三元运算符采用三个操作数。
4.6 像switch语句一样使用match表达式
问题
你有一种情况,你想创建一个简单的基于Java整数的switch语句,例如匹配一周中的天数、一年中的月数,以及一个整数映射到某个结果的其他场景。
解决方案
需要使用Scala的match表达式,如简单的基于整数的switch语句,请使用以下方法:
import scala.annotation.switch
// `i` 是一个整数
(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")
// 使用变量捕获默认值,以便你可以打印它
case whoa => println(s"Unexpected case: ${whoa.toString}")
该示例展示了如何基于match执行副作用操作(println)。一个更加函数式的方法是从match表达式返回一个值:
import scala.annotation.switch
// `i` 是一个整数
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"// 默认,包罗万象
@switch 注解
在编写这样的简单match表达式时,建议使用 @switch 注解,如下所示。如果switch无法编译为tableswitch或lookupswitch,此注释会在编译时提供警告。将match表达式编译为tableswitch或lookupswitch会更好地提高性能,因为它会生成分支表而不是决策树。当给表达式一个值时,它可以直接跳转到结果,而不是通过决策树。
Scala @switch 注释文档( https://oreil.ly/rrNBP )指出:
如果该注解存在,编译器将验证match是否已编译为tableswitch或lookupswitch,如果它编译为一系列条件表达式,则会发出错误。
可以通过一个简单的例子来展示 @switch 注解的效果。首先,将以下代码放入名为SwitchDemo.scala的文件中:
// 版本 1 - 编译为 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"
然后像往常一样编译代码:
$ scalac SwitchDemo.scala
编译此类不会产生警告并创建SwitchDemo.class输出文件。接下来,使用以下 javap 命令反汇编该文件:
$ javap -c SwitchDemo
这个命令行的输出给出了类似如下的tableswitch:
16: tableswitch { // 1 to 3
1: 44
2: 52
3: 60
default: 68
}
这表明 Scala 能够将match表达式优化为 tableswitch。(这是一件好事。)
接下来,对代码稍作改动,将整数文字 1 替换为一个值:
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"
同样,使用scalac编译代码,但你会立即看到一条警告消息:
$ scalac SwitchDemo.scala
SwitchDemo.scala:7: warning: could not emit switch for @switch annotated match
val x = (i: @switch) match {
^
one warning found
这条警告信息意味着既不能为match生成一个tableswitch,也不能为其生成一个lookupswitch。你可以通过运行 javap 的命令在生成的SwitchDemo.class文件上来确认这一点。当你查看该输出时,会看到前面示例中显示的tableswitch现在已消失。
Joshua Suereth在他的书Scala in Depth(Manning)中指出,在Scala中使用tableswitch优化必须满足以下条件:
匹配的值必须是已知的整数。
匹配的表达式必须是“简单的”。它不能包含任何类型检查、if 语句或提取器。
表达式的值必须在编译时可用。
应该有两个以上的 case 语句。
讨论
解决方案中的示例展示了两种处理默认“catch all”情况的方法。首先,如果你不关心默认匹配的值,你可以使用 _ 通配符来捕获它:
case _ => println("Got a default match")
相反,如果你对默认匹配的内容感兴趣,请为其分配一个变量名称。然后,你可以在表达式的右侧使用该变量:
case default => println(default)
使用像default这样的名称通常是最有意义的,但你可以为变量使用任何合法名称:
case oops => println(oops)
重要的是要知道,如果你不处理默认情况,你会得到MatchError。给定这个match表达式:
i match
case 0 => println("0 received")
case 1 => println("1 is good, too")
如果i是0或1以外的值,则表达式将引发MatchError:
scala.MatchError: 42 (of class java.lang.Integer)
at .<init>(<console>:9)
at .<clinit>(<console>)
much more error output here ...
因此,除非你有意编写偏函数,否则你将需要处理默认情况。
你真的需要match表达式吗?
请注意,对于这样的示例,你可能不需要match表达式。例如,任何时候你只是将一个值映射到另一个值,最好使用Map:
val days = Map(
0 -> "Sunday",
1 -> "Monday",
2 -> "Tuesday",
3 -> "Wednesday",
4 -> "Thursday",
5 -> "Friday",
6 -> "Saturday"
)
println(days(0)) // prints "Sunday"
另见
有关JVM开关如何工作的更多信息,请参阅编译开关的JVM规范( https://oreil.ly/oUcwX )。
关于lookupswitch 和tableswitch 之间的区别,这个Stack Overflow页面( https://oreil.ly/JvE3P )指出,“区别在于lookupswitch 使用带有键和标签的表,而tableswitch 只使用带有标签的表。” 同样,有关更多详细信息,请参阅Java 虚拟机 (JVM) 规范的“Compiling Switches”部分( https://oreil.ly/oUcwX )。
有关偏函数的更多信息,请参阅10.7小节“创建偏函数”。
4.7 用一个case语句匹配多个条件
问题
你需要在match的多个匹配条件中执行相同的业务逻辑,而不是为每个case重复你的业务逻辑,你希望为匹配条件使用同一个业务逻辑代码。
解决方案
将执行相同业务逻辑的匹配条件放在一行,用 | 分隔(管道)字符:
// `i` 是一个 Int
i match
case 1 | 3 | 5 | 7 | 9 => println("odd")
case 2 | 4 | 6 | 8 | 10 => println("even")
case _ => println("too big")
相同的语法适用于字符串和其他类型。下面是一个基于字符串匹配的例子:
val cmd = "stop"
cmd match
case "start" | "go" => println("starting")
case "stop" | "quit" | "exit" => println("stopping")
case _ => println("doing nothing")
下面的例子展示了如何在每个case语句中匹配多个对象:
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")
如上所示,为每个case语句定义多个可能匹配项的能力可以简化你的代码。
另见
相关方法请参阅4.12小节。
4.8 将match表达式的结果分配给变量
问题
你想要从match表达式返回一个值并将其分配给变量,或者使用match表达式作为方法的主体。
解决方案
要将match表达式的结果分配给变量,请在表达式之前创建变量并赋值,如下面例子中的变量evenOrOdd:
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"
这种方法通常用于创建短方法或函数。例如,下面的方法实现了Perl对true和false的定义:
def isTrue(a: Matchable): Boolean = a match
case false | 0 | "" => false
case _ => true
讨论
你可能听说Scala是一种面向表达式的编程(expression-oriented programming(EOP))语言。EOP意味着每个构造都是一个表达式,产生一个值,并且没有副作用。与其他语言不同,在Scala中,每个构造(如if、match、for和try)都会返回一个值。有关更多详细信息,请参阅24.3小节。
4.9 在match表达式中访问默认情况的值
问题
你希望在使用match表达式时访问默认“catch all”的case表达式的值,但当前在使用 _ 通配符语法匹配时无法访问该值。
解决方案
不要使用 _ 通配符,而是为默认case分配一个变量名:
i match
case 0 => println("1")
case 1 => println("2")
case default => println(s"You gave me: $default")
通过给默认匹配项指定一个变量名,你可以在表达式右侧访问该变量。
讨论
这种技巧的关键是使用一个变量名作为默认匹配,而不是通常的 _ 通配符。你分配的名称可以是任何合法的变量名称,因此你可以将其命名为其他名称,而不是default名称,例如waht:
i match
case 0 => println("1")
case 1 => println("2")
case what => println(s"You gave me: $what" )
提供默认匹配很重要。不这样做可能会导致MatchError:
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 ...
有关MatchError的更多详细信息,请参阅4.6小节的讨论。
4.10 在match表达式中使用模式匹配
问题
你需要在match表达式中匹配一个或多个模式,该模式可以是常量模式、变量模式、构造函数模式、序列模式、元组模式或类型化模式。
解决方案
为要匹配的每个模式定义一个case语句。 以下方法显示了你可以在match表达式中使用的许多不同类型的模式的示例:
def test(x: Matchable): String = x match
// 常量模式
case 0 => "zero"
case true => "true"
case "hello" => "you said 'hello'"
case Nil => "an empty List"
// 序列模式
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 (a, b) => s"got $a and $b"
case (a, b, c) => s"got $a, $b, and $c"
// 构造函数模式
case Person(first, "Alexander") => s"Alexander, first name = $first"
case Dog("Zeus") => "found a dog named Zeus"
// 类型化模式
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
// 默认通配符模式
case _ => "Unknown"
end test
上述方法中的大match表达式显示了Programming in Scala一书中描述的不同类别的模式,包括常量模式、序列模式、元组模式、构造函数模式和类型化模式。
以下代码展示了 match 表达式中的所有情况,每个表达式的输出显示在注释中。 注意println方法是在导入时重命名使得例子更简洁:
import System.out.{println => p}
case class Person(firstName: String, lastName: String)
case class Dog(name: String)
// 触发常量模式
p(test(0)) // zero
p(test(true)) // true
p(test("hello")) // you said 'hello'
p(test(Nil)) // an empty List
// 触发序列模式
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
// 触发元组模式
p(test((1,2))) // got 1 and 2
p(test((1,2,3))) // got 1, 2, and 3
// 触发构造函数模式
p(test(Person("Melissa", "Alexander"))) // Alexander, first name = Melissa
p(test(Dog("Zeus"))) // found a dog named Zeus
// 触发类型化的模式
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)
// 触发通配符模式
p(test("33d")) // you gave me this string: 33d
请注意,在 match 表达式中,List和Map表达式是这样编写的:
case m: Map[_, _] => m.toString
case list: List[_] => s"thanks for the List: $list"
也可以写成如下的形式:
case m: Map[A, B] => m.toString
case list: List[X] => s"thanks for the List: $list"
我更喜欢下划线语法,因为它清楚地表明我不关心List或Map中存储的内容。实际上,有时我可能对存储在List或Map中的内容感兴趣,但由于JVM中的类型擦除,这成为一个难题。
类型擦除 -- TODO 耗子栏
当我第一次写这个例子的时候,我写的List表达式如下:
case l: List[Int] => "List"
如果你熟悉Java平台上的类型擦除,你可能知道这是行不通的。Scala编译器通过以下警告消息让你了解此问题:
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]"
^
如果你不熟悉类型擦除,我在本章节的查看更多部分中包含了一个链接,该链接的网页描述了它是如何在JVM上工作的。
讨论
通常,当使用这种技术时,你的方法将期望一个从基类或特质继承的实例,然后你的case语句将引用该基类型的子类型。这是在测试方法中推断出来的,其中每个Scala类型都是 Matchable 的子类型。下面的代码展示了一个更明显的例子。
在我的Blue Parrot 应用程序( https://alvinalexander.com/blueparrot )中,它要么播放声音文件,要么以随机的时间间隔“说出”它给出的文本,如下方法所示:
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)
makeRandomNoise方法携带一个RandomThing类型的参数,且match表达式处理它的两个子类型RandomFile和RandomString的方法。
模式
解决方案中的大match表达式显示了在 Programming in Scala 一书中定义的各种模式(由Scala语言的创建者Martin Odersky合著)。这些模式包括:
常量模式
变量模式
构造函数模式
序列模式
元组模式
类型化模式
变量绑定模式
这些模式将在以下段落中进行简要描述。
常量模式
一个常量模式只能匹配它自己。任何文字都可以用作常量。如果将 0 指定为常量,则只会匹配为 0 的 Int 值。例如:
case 0 => "zero"
case true => "true"
变量模式
这并没有在解决方案的大match示例中演示,但匹配任何对象的一个变量模式,就像 _ 通配符一样。可以被Scala绑定到任何对象,这使你可以在case语句的右侧使用变量。例如,在match表达式的末尾,你可以像这样使用 _ 通配符来捕获其他任何内容:
case _ => s"Hmm, you gave me something ..."
但是使用变量模式,你可以改为:
case foo => s"Hmm, you gave me a $foo"
更多信息请参阅4.9小节。
构造函数模式
构造函数模式允许你在 case 语句中匹配构造函数。如示例所示,你可以根据需要指定常量或变量模式:
case Person(first, "Alexander") => s"found an Alexander, first name = $first"
case Dog("Zeus") => "found a dog named Zeus"
序列模式
你可以匹配List、Array、Vector等序列。使用 _ 字符代表序列中的一个元素,使用 _* 代表零个或多个元素,如示例中所示:
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"
元组模式
如示例中所示,你可以匹配元组模式并访问元组中每个元素的值。如果你对值不感兴趣,也可以使用 _ 通配符:
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"
类型化模式
在以下示例中,str: String是类型化模式,str是模式变量:
case str: String => s"you gave me this string: $str"
如示例所示,你可以在声明后访问表达式的模式变量。
变量绑定模式
有时你可能希望将变量添加到模式中。你可以使用以下通用语法来执行此操作:
case variableName @ pattern => ...
这称为变量绑定模式。使用时,将match表达式的输入变量与模式进行比较,如果匹配,则将输入变量绑定到variableName。
通过展示它解决的问题可以最好地体现它的有用性。假设你有前面演示的 List 模式:
case List(1, _*) => "a list beginning with 1, having any number of elements"
正如展示的那样,这让你可以匹配第一个元素为1的List,但到目前为止,还没有在表达式的右侧访问List。访问列表时,你知道可以这样做:
case list: List[_] => s"thanks for the List: $list"
所以看起来你应该用序列模式试试这个:
case list: List(1, _*) => s"thanks for the List: $list"
不幸的是,这会失败并出现以下编译错误:
Test2.scala:22: error: '=>' expected but '(' found.
case list: List(1, _*) => s"thanks for the List: $list"
^
one error found
这个问题的解决方案是在序列模式中添加一个变量绑定模式:
case list @ List(1, _*) => s"$list"
此代码能够完成编译并按预期工作,你也可以在语句的右侧访问该List。
以下代码展示了该示例以及该处理方式的高效:
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)"
在match表达式内的两个List示例中,注释掉的代码行是无法编译的,但第二行展示了如何匹配所需的List对象,然后将该列表绑定到变量x。当这行代码匹配像 List(1,2,3) 这样的列表时,它会产生输出 List(1, 2, 3),如第一个println语句的输出所示。
第一个Some示例表示你可以将Some按所示方式匹配,但你无法在右侧的表达式访问其信息。第二个示例展示了如何访问Some中的值,第三个示例更进一步,让你可以访问Some对象本身。当它与第二个println调用匹配时,它会打印Some(foo),表明你现在可以访问Some对象。
最后,此方法用于匹配姓氏为 Doe 的 Person。此语法允许你将模式匹配的结果分配给变量p,然后在表达式的右侧访问该变量。
在match表达式中使用Some和None
为了完善这些示例,你经常将Some和None与match表达式一起使用。
例如,当你尝试使用类似toIntOption的方法从字符串创建数字时,你可以在match表达式中处理结果:
val s = "42"
// later in the code ...
s.toIntOption match
case Some(i) => println(i)
case None => println("That wasn't an Int")
在match表达式中,你只需指定Some和None情况,如上所示来处理成功和失败条件。参阅24.6小节“使用 Scala 的错误处理类型(Option、Try 和 Either)”,以了解更多使用Option、Some和None的示例。
另见
Stack Overflow( http://bit.ly/15odxST )上的讨论,在使用match表达式时绕过类型擦除
My Blue Parrot应用程序( https://alvinalexander.com/blueparrot )
类型擦除文档( http://bit.ly/139WrFj )
4.11 在match表达式中使用枚举和样例类
问题
你想在match表达式中匹配枚举、样例类或样例对象。
解决方案
以下示例展示了如何使用模式匹配枚举的不同方式,具体取决于你在每个case语句右侧需要哪些信息。首先,这是一个名为Animal的枚举,它具有三个实例Dog、Cat和Woodpecker:
enum Animal:
case Dog(name: String)
case Cat(name: String)
case Woodpecker
给定该枚举,下面getInfo方法展示了在match达式中匹配枚举类型的不同方式:
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"
下面的例子展示了当给定Dog、Cat和Woodpecker时getInfo如何工作:
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
在getInfo中,如果Dog类匹配,则提取其名称并用于在表达式右侧创建字符串。为了展示提取名称时使用的变量名称可以是任何合法的变量名称,我使用名称moniker。
匹配Cat时,我想忽略名称,因此我使用上述的语法来匹配任何Cat实例。因为Woodpecker不是使用参数创建的,所以它也匹配,如上所示。
讨论
在 Scala 2 中,sealed traits与case class和case object一起使用,以实现与枚举相同的效果:
sealed trait Animal
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal
case object Woodpecker extends Animal
如6.12小节“如何使用枚举创建命名值集”所述,枚举是(a)定义sealed class或trait以及(b)定义为类的伴随对象成员的值的快捷方式。这两种方法都可以在getInfo的match表达式中使用,因为case class有一个内置的unapply方法,可以让它们在match表达式中工作。我在7.8小节“使用unapply实现模式匹配”中描述了它是如何工作的。
4.12 将if表达式(守卫语句)添加到case语句中
问题
你希望将特定的逻辑添加到match表达式中的case语句,例如允许一定范围的数字或匹配模式,但前提是该模式与某些附加条件匹配。
解决方案
在你的case语句中添加一个if守卫语句。用它来匹配一系列数字:
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...")
用它来匹配对象的不同值:
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")
只要你的类有一个unapply方法,你就可以在你的if守卫语句中引用类字段。例如,因为一个样例类有一个自动生成的unapply方法,给定这个Stock类和实例:
case class Stock(symbol: String, price: BigDecimal)
val stock = Stock("AAPL", BigDecimal(132.50))
你可以在类变量中使用模式匹配和守卫语句条件:
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
你还可以从case class和已正确实现unapply方法的类中提取字段,并在你的守卫语句条件中使用这些字段。例如:
// 提取“case”中的“name”,然后使用该值
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!")
如果将Person定义为case class,则可以正常运行:
case class Person(aName: String)
或者正确实现了unapply方法的Person类:
class Person(val aName: String)
object Person:
// 'unapply' 解构 Person。它也被称为
// 提取器,Person 是一个“提取器对象”。
def unapply(p: Person): Option[String] = Some(p.aName)
有关如何编写unapply方法的更多详细信息,请参阅7.8小节“使用unapply实现模式匹配”。
讨论
每当你想在case语句的左侧(即 => 符号之前)添加条件推断时,都可以使用这样的if表达式。
请注意,所有这些示例都可以通过将if推断放在表达式的右侧来编写,如下所示:
case Person(name) =>
if name == "Fred" then println("Yabba dabba doo")
else if name == "Bam Bam" then println("Bam bam!")
但是,在许多情况下,通过将if守卫语句直接与case语句相结合,你的代码将更简单、更易于阅读;它有助于将守卫与后来的业务逻辑分开。
另请注意,这个Person示例有点刻意,因为Scala的模式匹配功能让你可以像这样编写案例:
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!")
在这种情况下,当Person更复杂并且你需要做的不仅仅是匹配其参数时,确实需要一个守卫语句。
此外,如4.10小节中所示,与其使用如下的解决方案:
case x if (x == 2 || x == 3) => println(x)
还有另一种可能的解决方案是使用变量绑定模式:
case x @ (2|3) => println(x)
这段代码可以理解为:“如果match表达式的值 x 是 2 或 3,则将该值赋给变量x,然后使用println打印 x。”
4.13 使用match表达式代替isInstanceOf
问题
你想编写一段代码来匹配一种类型或多种不同的类型。
解决方案
你可以使用isInstanceOf方法来测试对象的类型:
if x.isInstanceOf[Foo] then ...
然而,“Scala方式”更倾向于match表达式这种工作方式,因为使用match表达式通常比isInstanceOf更强大和更方便。
例如,在一个基本用例中,你可能会收到一个未知类型的对象,并希望确定该对象是否是Person的实例。这段代码展示了如何编写一个match表达式,如果类型是Person,则返回true,否则返回false:
def isPerson(m: Matchable): Boolean = m match
case p: Person => true
case _ => false
更常见的场景是你将有一个这样的模型:
enum Shape:
case Circle(radius: Double)
case Square(length: Double)
然后你会想写一个方法来计算Shape的面积。此问题的一种解决方案是使用模式匹配来编写area方法:
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
这是一种常见的用法,其中area接受一个参数,其类型是你在match中解构类型的直接父级。
请注意,如果Circle和Square采用额外的构造函数参数,并且你只需要访问它们的半径和长度,则完整的解决方案如下所示:
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
如match表达式中的case语句所示,只需忽略不需要的参数,用 _ 字符指代它们。
讨论
如上所示,match表达式允许你匹配多种类型,因此使用它来替换isInstanceOf方法只是对match/case语法和Scala应用程序中使用的一般模式匹配方法的自然使用。
对于最基本的用例,isInstanceOf方法可以是一种更简单的方法来确定一个对象是否匹配一个类型:
if (o.isInstanceOf[Person]) { // handle this ...
但是,对于比这更复杂的场景,match表达式比长if/then/else if语句更具可读性。
面向对象中的isInstanceOf -- TODO 方块中
在面向对象编程中,使用isInstanceOf可能表明你没有正确使用继承。例如,有人可能写过这样的代码:
enum Animal:
case Cat, Dog, Ostrich
如果由于某种原因你发现自己正在编写这样的isInstanceOf代码,这表明你可能做错了:
if currentInstance.isInstanceOf[Ostrich] then ...
相反,OOP代码看起来像这样:
val animal: Animal =
AnimalFactory.getAnimal("big flightless bird")
// some time later in your code ...
animal.walk()
或者这样:
val oz: Ostrich =
AnimalFactory.getAnimal(
"big flightless bird"
).asInstanceOf[Ostrich]
// some time later in your code ...
oz.tryToFly()
如果你写这样的代码来接收一个Animal,代码不应该关心是否你收到的动物是Cat、Dog或Ostrich,如果你立即强制转换该实例到Ostrich,你知道你可以调用它的任何其他方法。因此,在OOP中,你应该很少需要使用isInstanceOf测试实例。相反,在函数式编程代码中,可以一直使用match表达式的模式匹配来处理类型。
另见
4.10小节展示了更多的 match 的技术。
4.14 在match表达式中使用列表
问题
你知道List数据结构与其他顺序数据结构有点不同:它由cons单元构建,并以Nil元素结束。你想要结合match表达式时来利用这个优势, 例如在编写递归时功能。
解决方案
你可以创建一个包含整数1、2和3的列表,如下所示:
val xs = List(1, 2, 3)
或像这样:
val ys = 1 :: 2 :: 3 :: Nil
如第二个示例所示,List以Nil元素结尾,你可以在编写match表达式利用这个优势来处理列表,尤其是当编写递归算法。例如,在下面的listToString方法中,如果当前元素不是Nil,该方法被递归调用剩余的List,但如果当前元素为Nil,则停止递归调用并返回返回空字符串:
def listToString(list: List[String]): String = list match
case s :: rest => s + " " + listToString(rest)
case Nil => ""
REPL展示了此方法的是如何工作的:
scala> val fruits = "Apples" :: "Bananas" :: "Oranges" :: Nil
fruits: List[java.lang.String] = List(Apples, Bananas, Oranges)
scala> listToString(fruits)
res0: String = "Apples Bananas Oranges "
在处理其他类型和不同算法的列表时,可以使用相同的方法。例如,虽然你可以只编写List(1,2,3).sum,但此示例展示了如何使用匹配和递归编写你自己的sum方法:
def sum(list: List[Int]): Int = list match
case Nil => 0
case n :: rest => n + sum(rest)
同样,这是一个乘积算法:
def product(list: List[Int]): Int = list match
case Nil => 1
case n :: rest => n * product(rest)
REPL展示了这些方法是如何工作的:
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
不要忘记reduce和fold操作 -- TODO 耗子栏
虽然递归很棒,但Scala在集合类上的各种reduce和fold方法是为了让你在遍历集合的同时应用算法,且它们通常不需要递归。
例如,你可以使用以下两种形式中的任何一种来编写求和算法:
// long form
def sum(list: List[Int]): Int = list.reduce((x,y) => x + y)
// short form
def sum(list: List[Int]): Int = list.reduce(_ + _)
有关详细信息,请参阅13.10小节“使用reduce和fold方法遍历集合”。
讨论
如上所示,递归是一种方法调用自身以解决问题的技术。在函数式编程中——所有变量都是不可变的——递归提供了一种迭代List中的元素以解决问题的方法,例如计算List中所有元素的总和或乘积。特别是使用List类的一个好处是List以Nil元素结尾,因此你的递归算法通常具有以下模式:
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
函数式编程中的变量 -- TODO 耗子栏
在FP中,我们使用术语变量,但当我们只使用不可变变量时,这个词似乎没有什么意义,即我们有一个不能变化的变量。
这里发生的事情是,我们真正的意思是代数意义上的“变量”,而不是计算机编程意义上的“变量”。例如,在代数中,当我们写下这个代数方程时,我们说a、b和c是变量:
a = b * c
但是,一旦它们被分配,它们就不能改变。术语变量在函数式编程中具有相同的含义。
另见
我最初发现递归是一个不必要的难以掌握的话题,所以我写了很多关于它的博客文章:
“Scala Recursion Examples (Recursive Programming)” ( https://oreil.ly/5LASz )
“Recursive: How Recursive Function Calls Work”( https://oreil.ly/vIfgS )
“Tail-Recursive Algorithms in Scala”( https://oreil.ly/wJat4 )
“Recursion: Visualizing the Recursive sum Function”( https://oreil.ly/wJat4 )
在“Recursion: Thinking Recursively”( https://oreil.ly/0grXf )中,我写了特征(identity)元素,包括 0 是和运算的特征元素,1 是乘运算的特征元素,而""(一个空字符串)是处理字符串的特征元素。
4.15 用try/catch匹配一个或多个异常
问题
你想在try/catch块中捕获一个或多个异常。
解决方案
Scala的try/catch/finally语法类似于Java,但它在catch块中使用了match表达式:
try
doSomething()
catch
case e: SomeException => e.printStackTrace
finally
// do your cleanup work
当你需要捕获和处理多个异常时,只需将异常类型添加为不同的case语句即可:
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.")
如果你愿意,也可以像这样编写代码:
try
openAndReadAFile(filename)
catch
case e: (FileNotFoundException | IOException) =>
println(s"Had an IOException trying to read $filename")
讨论
综上所述,Scala的case语法用于匹配不同的可能出现的异常。如果你不关心抛出哪些特定的异常,并且想捕获所有异常并对其进行处理(例如记录它们),请使用以下语法:
try
openAndReadAFile(filename)
catch
case t: Throwable => logger.log(t)
由于某种原因,如果你不关心异常的值,你也可以像这样捕获它们并忽略它们:
try
openAndReadAFile(filename)
catch
case _: Throwable => println("Nothing to worry about, just an exception")
基于try/catch的方法
如本章的介绍所示,try/catch/finally块可以返回一个值,因此可以用作方法的主体。以下方法返回一个 Option[String]。如果找到文件,它返回一个包含字符串的 Some,如果读取文件有问题,则返回None:
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
这展示了一种从try表达式返回值的方式。
这些天来,我很少编写抛出异常的方法,但是像Java一样,你可以从catch子句中抛出异常。然而,因为Scala没有检查异常,你不需要指定一个方法抛出的异常。如下例子所示,该方法不包含任何注解:
// 危险:此方法不会警告你可能会抛出异常
def readFile(filename: String): String =
try
Source.fromFile(filename).getLines.mkString
catch
case t: Throwable => throw t
这实际上是一种非常危险的方法 —— 不要写这样的代码!
要声明方法抛出异常,请将 @throws 注解添加到方法定义中:
// 更好:这个方法警告其他人可以抛出异常
@throws(classOf[NumberFormatException])
def readFile(filename: String): String =
try
Source.fromFile(filename).getLines.mkString
catch
case t: Throwable => throw t
虽然最后一种方法比前一种方法好,但两者都不是首选的方法。 “Scala 方式”是从不抛出异常。相反,你应该使用Option,如前所示,或者当你想要返回有关失败的信息时,使用Try/Success/Failure或Either/Right/Left类。下面的例子展示了如何使用Try:
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)
每当涉及异常消息时,我总是更喜欢使用Try或Either而不是Option,因为它们使你可以访问Failure或Left中的消息,其中Option仅返回None。
捕捉一切的简洁方法
捕获所有异常的另一种简洁方法是使用scala.util.control.Exception对象的allCatch方法。以下示例展示了如何使用 allCatch,先展示成功案例,再展示失败案例。每个表达式的输出放在每一行的注释之后:
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")
另见
有关声明方法可能抛出异常的更多示例,请参阅8.7小节“声明方法可以抛出异常”。
有关使用 Option/Some/None 和 Try/Success/Failure 的更多信息,请参阅24.6小节“使用Scala的错误处理类型(Option、Try 和 Either)”。
有关 allCatch 的更多信息,请参阅 scala.util.control.Exception 的Scaladoc页面( https://oreil.ly/6pUah )。
4.16 在try/catch/finally块中使用变量之前先声明该变量
问题
你想在try块中使用一个对象,并且需要在finally代码中访问它,例如当你需要调用某个对象的close方法时。
解决方案
通常,在try/catch块之前将你的字段声明为Option,然后在try子句中将变量绑定到一个Some。如下所示,其中sourceOption字段声明在try/catch块之前,并在try子句中进行赋值:
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
这是一个预先写好的例子 —— 16.1小节“读取文本文件”,展示了很多更好的读取文件的方法 —— 但它确实展示了这种方式。首先,在try块之前定义一个var字段作为Option:
var sourceOption: Option[Source] = None
然后,在try子句中,将一个Some值赋值给该变量:
sourceOption = Some(Source.fromFile("/etc/passwd"))
当你有一个资源要关闭时,使用像这样的技术(尽管 16.1 节“读取文本文件”也展示了一种更好的关闭资源的方法)。请注意,如果在此代码中抛出异常,则finally里面的sourceOption将为None值。如果没有抛出异常,将执行match表达式的Some分支。
讨论
本小节的一个关键点是了解声明未初始化的Option字段的语法:
var in: Option[FileInputStream] = None
var out: Option[FileOutputStream] = None
也可以使用第二种形式,但首选第一种形式:
var in = None: Option[FileInputStream]
var out = None: Option[FileOutputStream]
不要使用null值
当我第一次开始使用Scala时,我认为编写此代码的唯一方法是使用 null 值。以下代码展示了我在检查电子邮件帐户的应用程序中使用的方法。此代码中的store和inbox字段被声明为具有Store和Folder类型的 null 字段(来自javax.mail包):
// (1) 声明 null 变量(不要使用 null;这只是一个例子)
var store: Store = null
var inbox: Folder = null
try
// (2) 使用 try 块中的变量/字段
store = session.getStore("imaps")
inbox = getFolder(store, "INBOX")
//这里的其余代码...
catch
case e: NoSuchProviderException => e.printStackTrace
case me: MessagingException => me.printStackTrace
finally
// (3) 在 finally 子句中的对象上调用 close()
if (inbox != null) inbox.close
if (store != null) store.close
但是,在Scala工作中,甚至让你有机会忘记空值的存在,因此不推荐使用这种方法。
另见
有关 (a) 如何不使用 null 值以及 (b) 如何使用 Option、Try 和 Either 的更多详细信息,请参阅这些章节:
24.5小节,“消除代码中的null值”
24.6小节,“使用Scala的错误处理类型(Option、Try 和 Either)”
24.8小节,“使用高阶函数处理Option值”
每当你编写需要在启动时打开资源并在完成时关闭资源的代码时,使用 scala.util.Using 对象( https://oreil.ly/N47eZ )会很有帮助。参阅16.1小节“读取文本文件”,以了解如何使用这个对象以及更好的读取文本文件的方法。
此外,24.8小节“使用高阶函数处理Option值”,展示了除了使用match表达式之外的其他处理Option值的方法。
4.17 创建自己的控制结构
问题
你想定义自己的控制结构,简化代码或创建领域特定语言 (DSL)。
解决方案
得益于多参数列表、传名参数、扩展方法、高阶函数等功能,你可以自定义创建一个像控制结构一样的有效代码。
例如,假设 Scala 没有自己的内置 while 循环,而你想创建自己的自定义whileTrue循环,可以像这样使用它:
var i = 0
whileTrue (i < 5) {
println(i)
i += 1
}
要创建此whileTrue控制结构,请定义一个名为whileTrue的方法,该方法采用两个参数列表。第一个参数处理判断条件——在该示例中,i < 5——第二个参数是用户想要运行的代码块,即大括号之间的代码。将这两个参数定义为传名参数。因为whileTrue只用于副作用,比如更新可变变量或打印到控制台,所以声明它返回Unit。方法大致的签名如下所示:
def whileTrue(testCondition: => Boolean)(codeBlock: => Unit): Unit = ???
实现方法主体的一种方法是编写递归算法。此代码展示了一个完整的解决方案:
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
在这段代码中,对testCondition求值,如果条件为真,则执行codeBlock,然后递归调用whileTrue。它一直在调用自己,直到测试条件返回false。
要测试此代码,首先将其导入:
import WhileTrue.whileTrue
然后运行前面显示的whileTrue循环,你会看到它的工作原理是符合预期的。
讨论
Scala语言的创建者有意识地决定不在Scala中实现某些关键字,而是通过Scala库实现功能。例如,Scala没有内置的break和continue关键字。相反,它通过一个库来实现它们,正如我在我的博客文章 “Scala:如何在 for 和 while 循环中使用 break 和 continue”( https://oreil.ly/KOAHI )中所描述的那样。
如解决方案中所示,创建自己的控制结构的能力来自以下功能:
多个参数列表让你可以像我对whileTrue 所做的那样:创建一个参数组用于测试条件,第二组用于代码块。
传名参数还可以让你做我对whileTrue 所做的事情:接受的参数在方法内部访问它们之前是不会对其进行评估的。
类似地,中缀表示法、高阶函数、扩展方法和流式接口等其他特性允许你创建其他自定义控制结构和 DSL。
传名参数
传名参数是whileTrue控制结构的重要组成部分。在Scala中,重要的是要知道使用 => 定义方法参数的语法:
def whileTrue(testCondition: => Boolean)(codeBlock: => Unit) =
----- -----
你正在创建所谓的传名调用或传名参数。仅当在你的方法内部访问时才评估计算传名参数,因此,正如我所写的博客文章“如何在Scala中使用传名参数”( https://oreil.ly/fuWGM )和“Scala和传名调用参数”( https://oreil.ly/shdre ),这些参数的更准确的名称是“访问时求值”。那是因为这正是它们的工作方式:只有在方法内部访问它们时才会评估计算它们。正如我在第二篇博文中所指出的,Rob Norris进行了比较,即传名参数就像接收一个def方法。
另一个例子
在whileTrue示例中,我使用递归调用来保持循环运行,但对于更简单的控制结构,你不需要递归。例如,假设你想要一个接受两个测试条件的控制结构,如果两个测试条件都为真,就运行提供的代码块。使用该控制结构的表达式如下所示:
doubleIf(age > 18)(numAccidents == 0) { println("Discount!") }
在这种情况下,将doubleIf定义为采用三个参数列表的方法,每个参数都是一个传名参数:
// 两个“if”条件测试
def doubleIf(test1: => Boolean)(test2: => Boolean)(codeBlock: => Unit) =
if test1 && test2 then codeBlock
因为doubleIf只需要执行一次测试,不需要无限循环,因此不需要在其方法体中进行递归调用。它只是检查两个测试条件,如果它们都为true,则执行codeBlock。
另见
David Pollak (Apress) 所著的 Beginning Scala 一书中展示了我最喜欢使用这种技术的一个方法。虽然它已被scala.util.Using废弃对象,我在这篇博文“The using Control Structure in Beginning Scala“( https://oreil.ly/fiLHH )中描述了该技术的工作原理。
Scala Breaks 类用于在for循环中实现break和continue功能,我写了一篇关于它的文章:“Scala: How to Use break and continue in for and while Loops”( https://oreil.ly/KOAHI )。Breaks类源代码相当简单,并提供了另一个如何实现控制结构的示例。你可以在其Scaladoc页面( https://oreil.ly/xI78S )上以找到其源代码。
最后更新于
这有帮助吗?