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)
}
}
✅ 这个方法将返回 Success
或 Failure
类型。借助模式匹配(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
还提供了 opt
和 either
方法,可以分别将异常包装为 Option
或 Either
。
💡 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!")
}
}
✅ 在这个例子中,我们直到最后才决定如何处理异常。 编写 trySuccessFailure
和 customCatchObjects
时,并不需要提前考虑异常处理逻辑。
✅ Try/Success/Failure
和 catch 对象都支持函数式组合,它们让我们可以延迟对异常的处理,而不是像传统 try/catch
那样立即处理。
8. 总结
在这篇文章中,我们介绍了 Scala 中的异常处理机制。
在选择使用 catch 对象还是 Try/Success/Failure
时,需要权衡代码可读性和复用性。
在 Scala 中,这两种方式都比传统的 try/catch/finally
更推荐使用,因为它们能更自然地支持函数式编程风格,帮助我们写出更具组合性的代码。
一如既往,本文中的完整代码可以在 GitHub 上找到。