与Java 不同,Scala 并不支持已被视为失败设计的检查型异常(checked exception)。Scala 将Java 中的检查型异常视为非检查型,而且方法声明中也不包含throw 子句。不过Scala 提供了有助于Java 互操作的@throws 注解.
Scala 将异常处理作为另一类模式匹配来进行处理,因此我们可以简洁地对各种不同类型的异常进行处理。
让我们看看Scala 在资源管理这样一个常见的应用场景中是如何处理异常的。我们希望通过某种方式打开并处理一些文件。在这个示例中我们仅仅会统计行数。不过,我们仍然必须对一些错误场景进行处理。比如说,文件也许并不存在,这个错误尤其是当我们需要让用户指定文件名时尤为明显。除此之外,处理文件时可能也会有某些错误(为了测试错误发生的场景,我们将随意地触发一个错误)。无论是否成功地对文件进行了处理,我们都需要确保关闭了所有的文件句柄。
object TryCatch { /** Usage: scala rounding.TryCatch filename1 filename2 ... */ def main(args: Array[String]) = { args foreach (arg => countLines(arg)) // 使用foreach 循环遍历参数列表并对各个参数进行处理。该循环每遍历一次便返回一个Unit 对象,而foreach 执行完毕后所返回的最终结果也是Unit 对象。 } import scala.io.Source // 导入用于读取输入的scala.io.Source 类 import scala.util.control.NonFatal def countLines(fileName: String) = { // 统计每个文件名所对应文件的行数。 println() // Add a blank line for legibility var source: Option[Source] = None // 由于我们将变量source 声明为Option 类型,因此我们在finally 子句中能分辨出 source 对象是否是真正的实例。 try { // 开始执行try 子句。 source = Some(Source.fromFile(fileName)) // 假如文件不存在,source.fromFile 方法将抛出java.io.FileNotFoundException类型的异常。否则的话,我们将该方法的返回值封装到Some 对象中。下一行中我们将调用source 变量的 get 方法,由于目前我们已经能确认source 属于Some 类型,因此这一操作是安全的。 val size = source.get.getLines.size println(s"file $fileName has $size lines") } catch { case NonFatal(ex) => println(s"Non fatal exception! $ex") // 捕获那些非致命的错误。例如,内存不足是一个致命错误。 } finally { for (s <- source) { // 使用for 推导式从Some 类型的对象中提取Source 实例,之后将关闭文件。假如source对象为None,将不会发生任何事。 println(s"Closing $fileName...") s.close } } } }
请留意catch 子句。Scala 会使用模式匹配来捕捉你所希望捕获的异常,而Java 则使用单独的catch 子句来捕获各个异常。与Java 相比,Scala 捕获异常的方式更紧凑、更灵活。
在这段示例代码中,case NonFatal(ex) => …子句使用scala.util.control.NonFatal 匹配了所有的非致命性异常。
应用finally 子句可以确保资源会在一处得到合理的清理。如果不使用finally,我们将不得不分别在try 子句和catch 子句中重复清理逻辑,这样才能确保文件句柄会被关闭。由于我们使用了for 推导式从option 对象中抽取出Source 对象,因此即使option 对象实际上是一个None 实例,什么也不会发生,包含了文件close 方法的代码块并不会被调用。
假如你需要对Option 对象进行检测,当它是Some 对象时执行一些操作,而当它是None 对象时则不进行任何操作,那么你可以使用for 推导式,这也是Scala 的一个广泛应用的常见用法。
由于该程序已经经过sbt 编译,因此我们可以在sbt 提示符后运行run-main 任务来启动该程序,启动run-main 任务时允许输入参数。为了方便阅读,我将输入参数折成多行,使用 \ 表示行符 ,并删除了一些文本。
> run-main progscala2.rounding.TryCatch foo/bar \ src/main/scala/progscala2/rounding/TryCatch.scala [info] Running rounding.TryCatch foo/bar .../rounding/TryCatch.scala ... java.io.FileNotFoundException: foo/bar (No such file or directory) file src/main/scala/progscala2/rounding/TryCatch.scala has 30 lines Closing src/main/scala/progscala2/rounding/TryCatch.scala... [success] ...
第一个文件foo/bar 并不存在,而第二个文件才是该程序的源文件。使用scala.io.SourceAPI 能够方便地对来自文件或其他源的数据流进行处理。与一些这类的API 相似,文件不存在时Source 将抛出异常。因此读取foo/bar 文件时抛出异常的行为是可预期的行为。
假如无论是否成功地使用了资源,资源都需要被清理,请将资源清理的相关逻辑放到finally 子句中执行。
除了使用模式匹配定位异常之外,Scala 异常处理的其他设定与大多数语言相似。与Java一样,使用Scala 时我们通过编写throw new MyBadExceptoin(...) 抛出异常。假如你自定义的异常是一个case 类,那么抛出异常时可以省略new 关键字。这就是Scala 与其他语言在异常处理上的差别。