7.4 用try表达式实现异常处理
Scala的异常处理跟其他语言类似。方法除了正常地返回某个值,也可以通过抛出异常终止执行。方法的调用方要么捕获并处理这个异常,要么自我终止,让异常传播到更上层调用方。异常通过这种方式传播,逐个展开调用栈,直到某个方法处理该异常或者再没有更多方法了为止。
抛出异常
在Scala中抛出异常跟Java看上去一样。你需要创建一个异常对象然后用throw关键字将它抛出:
虽然看上去有些自相矛盾,在Scala中throw是一个有结果类型的表达式。如下是一个带有结果类型的示例:
在这段代码中,如果n是偶数,half将被初始化成n的一半。如果n不是偶数,那么在half被初始化之前,就会有异常被抛出。因此,我们可以安全地将抛出异常当作任何类型的值来对待。任何想要使用throw给出的这个返回值的上下文都没有机会真正使用它,也就不必担心有其他问题。
从技术上讲,抛出异常这个表达式的类型是Nothing。哪怕表达式从不实际被求值,也可以用throw。这个技术细节听上去有点奇怪,不过在前一例这样的场景下,还是很常见也很有用的。if的一个分支计算出某个值,而另一个分支抛出异常并计算出Nothing。整个if表达式的类型就是那个计算出某个值的分支的类型。我们将在11.3节对Nothing做进一步的介绍。
捕获异常
可以用示例7.11中的语法来捕获异常。catch子句的语法之所以是这样,为的是与Scala的一个重要组成部分,模式匹配(pattern matching),保持一致。我们将在本章简单介绍并在第15章详细介绍模式匹配这个强大的功能。
示例7.11 Scala中的try-catch子句
这个try-catch表达式跟其他带有异常处理的语言一样。首先代码体会被执行,如果抛出异常,则会依次尝试每个catch子句。在本例中,如果异常的类型是FileNotFoundException,第一个子句将被执行。如果异常类型是IOException,那么第二个子句将被执行。而如果异常既不是FileNotFoundException也不是IOException,try-catch将会终止,异常将向上继续传播。
注意
你会注意到一个Scala跟Java的区别,Scala并不要求你捕获受检异常(checked exception)或在throws子句里声明。可以选择用@throws注解来声明一个throws子句,但这并不是必需的。关于@throws的详情,请参考31.2节。
finally子句
可以将那些不论是否抛出异常都想执行的代码以表达式的形式包在finally子句里。例如,你可能想要确保某个打开的文件要被正确关闭,哪怕某个方法因为抛出了异常而退出。示例7.12给出了这样的例子:[5]
示例7.12 Scala中的try-finally语句
注意
示例7.12展示了确保非内存资源被正确关闭的惯用做法,这些资源可以是文件、套接字、数据库连接等。首先获取资源,然后在try代码块中使用资源,最后在finally代码块中关闭资源。关于这个习惯Scala和Java是一致的。Scala提供了另一种技巧,贷出模式(loan pattern)来更精简地达到相同的目的。我们将在9.4节详细介绍贷出模式。
交出值
跟Scala的大多数其他控制结构一样,try-catch-finally最终返回一个值。例如,示例7.13展示了如何做到解析URL,但当URL格式有问题时返回一个默认的值。如果没有异常抛出,整个表达式的结果就是try子句的结果;如果有异常抛出并且被捕获时,整个表达式的结果就是对应的catch子句的结果;而如果有异常抛出但没有被捕获,整个表达式就没有结果。如果有finally子句,该子句计算出来的值会被丢弃。finally子句一般都用来执行清理工作,比如关闭文件。通常来说,它们不应该改变主代码体或catch子句中计算出来的值。
示例7.13 交出值的catch语句
如果你熟悉Java,需要注意的是Scala的行为跟Java不同,仅仅是因为Java的try-finally并不返回某个值。跟Java一样,当finally子句包含一个显式的返回语句,或者抛出某个异常,那么这个返回值或异常将会“改写”(overrule)任何在之前的try代码块或某个catch子句中产生的值。例如,在下面这个刻意做成这样的函数定义中:
调用f()将得到2。相反,如果是如下代码:
调用g()将得到1。这两个函数的行为都很可能让多数程序员感到意外。因此,最好避免在finally子句中返回值,最好将finally子句用来确保某些副作用发生,比如关闭一个打开的文件。