1. 引言

在本教程中,我们将深入探讨 Scala 中的异常处理机制。通过语言本身提供的多种结构,我们会介绍几种不同的异常处理方式。

2. 什么是异常?

异常是程序执行过程中发生的、会中断正常流程的事件。

异常处理(Exception Handling)是一种响应异常发生的方式。

Java 中的异常分为 检查型和非检查型,而 Scala 只支持非检查型异常。也就是说,在编译期我们无法知道某个方法是否会抛出未被捕获的异常。

3. 一个简单的计算器示例

我们来写一个简易计算器,用来演示 Scala 中不同异常处理方式的使用。
这个计算器只支持两个正整数相加:

object CalculatorExceptions {
  class IntOverflowException extends RuntimeException
  class NegativeNumberException extends RuntimeException
}

object Calculator {
  import CalculatorExceptions._

  def sum(a: Int, b: Int): Int = {
    if (a < 0 || b < 0) throw new NegativeNumberException
    val result = a + b
    if (result < 0) throw new IntOverflowException
    result
  }
}

我们的方法可能会抛出以下两种异常:

  • 如果其中一个加数为负数,则抛出 NegativeNumberException
  • 如果结果溢出 Int 的范围,则抛出 IntOverflowException

在使用这个计算器时,我们必须考虑如何妥善处理这些异常情况。

接下来的部分中,我们将展示几种在 Scala 中进行异常处理的方法。

4. try/catch/finally

Scala 提供了与 Java 类似的 try/catch/finally 结构来处理异常。

下面的例子中,为了方便测试,我们在捕获异常时返回不同的负值错误码:

def tryCatch(a: Int, b: Int): Int = {
  try {
    return Calculator.sum(a,b)
  } catch {
    case e: IntOverflowException => -1
    case e: NegativeNumberException => -2
  } finally {
    // 无论是否出现异常,该块都会被执行
    println("Calculation done!")
  }
}

需要注意的关键点包括:

  • “有风险”的代码放在 try 块中
  • finally 块中的代码无论如何都会执行 —— 比如用于关闭数据库连接等资源清理操作
  • catch 块中可以用 case 匹配不同的异常类型

⚠️ 使用这种方式,我们必须在异常抛出的第一时间就决定如何处理它。例如上面的例子中,我们根据捕获到的不同异常返回了不同的错误码。

5. Try/Success/Failure

Try[T] 是一种代数数据类型(ADT),其实例包括 Success[T]Failure[T]

让我们用它重写之前的 tryCatch 方法:

def trySuccessFailure(a: Int, b: Int): Try[Int] = Try {
  Calculator.sum(a,b)
}

现在我们可以用一些测试用例来看看 trySuccessFailure 是如何以函数式风格处理结果的:

"trySuccessFailure" should "handle NegativeNumberException" in {
  import CalculatorExceptions._
  val result = trySuccessFailure(-1,-2)
  result match {
    case Failure(e) => assert(e.isInstanceOf[NegativeNumberException])
    case Success(_) => fail("Should fail!")
  }
}

it should "handle IntOverflowException" in {
  import CalculatorExceptions._
  val result = trySuccessFailure(Int.MaxValue,1)
  result match {
    case Failure(e) => assert(e.isInstanceOf[IntOverflowException])
    case Success(_) => fail("Should fail!")
  }
}

it should "return the correct sum" in {
  import CalculatorExceptions._
  val result = trySuccessFailure(3,2)
  result match {
    case Failure(e) => fail("Should succeed!")
    case Success(result) => assert(result == 5)
  }
}

✅ 这个方法将返回 SuccessFailure 类型。借助模式匹配(pattern match),我们可以轻松地处理函数的结果。

6. Catch 对象

另一种异常处理方式来自 scala.util.control.Exception 对象。 我们可以用 catch 对象来封装 Calculator.sum 的调用:

def catchObjects(a: Int, b: Int): Try[Int] = allCatch.withTry {
  Calculator.sum(a,b)
}

allCatch.withTry 会捕获所有异常,并将其包装为 Try 类型。这段代码的行为与之前实现的 trySuccessFailure 完全一致。

此外,scala.util.control.Exception 还提供了 opteither 方法,可以分别将异常包装为 OptionEither

💡 Catch 对象的一个亮点是可以定义自定义的异常捕获器(matcher)。 下面我们来创建一个只捕获 NegativeNumberException 的自定义 matcher:

val myCustomCatcher = catching(classOf[NegativeNumberException])

def customCatchObjects(a: Int, b: Int): Try[Int] = myCustomCatcher.withTry{
  Calculator.sum(a,b)
}

另外,我们还可以使用 [scala.util.control.Exception.ignoring()] 来忽略指定的异常:

def ignoringAndSum(a: Int, b: Int) =
  ignoring(classOf[NegativeNumberException], classOf[IntOverflowException]) {
    println(s"Sum of $a and $b is equal to ${Calculator.sum(a, b)}")
  }

这段代码会在执行时忽略指定的异常。

下面是测试自定义 matcher 行为的样例:

"customCatchObjects" should "handle NegativeNumberException" in {
  import CalculatorExceptions._
  val result = customCatchObjects(-1,-2)
  result match {
    case Failure(e) => assert(e.isInstanceOf[NegativeNumberException])
    case Success(_) => fail("Should fail!")
  }
}

it should "handle IntOverflowException" in {
  import CalculatorExceptions._
  assertThrows[IntOverflowException] {
    customCatchObjects(Int.MaxValue,1)
  }
}

it should "return the correct sum" in {
  import CalculatorExceptions._
  val result = customCatchObjects(3,2)
  result match {
    case Failure(e) => fail("Should succeed!")
    case Success(result) => assert(result == 5)
  }
}
it should "ignore specified exceptions" in {
  Examples.ignoringAndSum(-1, -2)
}

❌ 当抛出 IntOverflowException 时,由于没有被自定义 matcher 捕获,因此不会被处理。

Catch 对象非常适合集中管理异常处理逻辑,避免重复代码。

⚠️ 不过,相比 Try/Success/Failure,Catch 对象的使用频率要低得多。

7. 函数组合性(Functional Composability)

到现在为止,我们已经能够推迟异常的处理时机。这是实现函数式组合性的关键所在。

我们可以把多个方法组合起来,看看如何在链式调用的最后统一处理异常:

"customCatchObjects composed with trySuccessFailure" should "return the correct sum" in {
  import CalculatorExceptions._
  val result = customCatchObjects(3, 2) flatMap (trySuccessFailure(_, 3))
  result match {
    case Failure(e)      => fail("Should succeed!")
    case Success(result) => assert(result == 8)
  }
}

it should "print an error" in {
  import CalculatorExceptions._
  val result = customCatchObjects(-1, -2) flatMap (trySuccessFailure(_, 3))
  result match {
    case Failure(e)      => println("Found error!")
    case Success(result) => fail("Should fail!")
  }
}

在这个例子中,我们直到最后才决定如何处理异常。 编写 trySuccessFailurecustomCatchObjects 时,并不需要提前考虑异常处理逻辑。

Try/Success/Failure 和 catch 对象都支持函数式组合,它们让我们可以延迟对异常的处理,而不是像传统 try/catch 那样立即处理。

8. 总结

在这篇文章中,我们介绍了 Scala 中的异常处理机制。

在选择使用 catch 对象还是 Try/Success/Failure 时,需要权衡代码可读性和复用性。

在 Scala 中,这两种方式都比传统的 try/catch/finally 更推荐使用,因为它们能更自然地支持函数式编程风格,帮助我们写出更具组合性的代码。

一如既往,本文中的完整代码可以在 GitHub 上找到。


原始标题:Exception Handling