编程语言提供的意外情况处理的机制大概分为两种:1. 函数返回「错误感知(error-aware)类型」的值;2. 抛出异常。

函数返回「错误感知类型」的值

在函数返回值中包含错误,这要求函数的调用者显式地处理这个错误,调用者可以选择从错误中恢复,或者继续传播这个错误。使用这种方法的一个重要前提是,函数的值必须被处理。在函数式风格的代码中,(几乎)所有的返回值都会被处理,但是在命令式风格的代码中,函数的返回值很容易被忽视掉。

具体到如何使用函数的返回值来表示意外情况,目前存在的一些方法有:

  • 使用一些「特殊」的值。例如,很多C函数通过返回-1或null来表示意外情况。这样一来,用于表示预期结果的值和用于表示意外情况的值就很容易混淆。
  • 通过多返回值的方式把预期结果和用于表示意外情况的值分离开来,这就是Golang的做法。在Golang里的写法大概是result, err := f(),result和err是两个变数,对result的使用不依赖对err的处理,所以这种做法其实并不严谨。
  • 通过类型把预期结果和可能发生的意外情况组合在一起,想要取出预期结果必须先对可能发生的意外情况进行处理。例如OCaml的option类型、Typed Racket的Union type。

前两种方法都是不严谨的,所有后文只谈第三种。

抛出异常

函数返回「错误感知类型」的值带来了一些麻烦,每一次函数调用都必须对返回值进行检查。所以很多现代化的语言都提供了异常机制,允许你「抛出异常」,函数的调用者可以「捕获」这个异常,对其进行处理,也可以忽视它,任其继续传播,直至「顶层」。

之前写了一篇文章,通过自己动手实现异常机制来理解异常。看完那篇文章之后应该能理解异常和普通的函数返回值之间的区别和联系。

孤独的Ziv:通过自己实现来理解异常?

zhuanlan.zhihu.com
图标

在两种方式之间切换

大部分提供了异常机制的语言都允许你在两种处理意外情况的方式之间切换,在两者之间做选择的时候需要考虑在简洁性和明确性之间权衡。

异常比较简洁,因为异常允许把错误处理的工作延迟到一个合适的地方,而不需要调用链上的每一个函数都处理一遍。而且,异常使用的是「nonlocal return」,不会和「local return」的类型混杂在一起。

和异常不同的是,错误感知类型则在类型定义中有充分的描述(不适用于C语言),使得代码可能产生的错误很明确,不会被忽略。

如何做出合适的权衡呢?这取决于你的具体应用。

下面的情况可能适合大量使用异常:

  • 任务是轻量级的,而且有「监督者」负责监控任务的状态并在任务失败后自动重启;
  • 「微服务」;
  • 程序宕掉的成本不高;
  • 「软实时」系统;

下面的情况可能需要使用错误感知返回类型:

  • 传统的「大型软体系统」的架构(有疑问?请继续看下一节);
  • 程序宕掉的成本很高;
  • 「硬实时」系统;

融合两种方式

使用错误感知返回类型要求函数的调用者必须处理意外情况,而且在类型定义中充分描述了可能产生的错误。异常机制带来了更多的灵活性,但是也带来了一些弱点:类型系统不知道函数可能抛出什么异常,异常容易被忽略掉。

Java这门语言试图对异常的弱点进行修复,它融合了上面的两种方式,在Java的设计中有一些特色:

  1. 必须给异常写类型标记(throws),而且函数/方法的普通返回值类型和「nonlocal return」(异常)的类型标记是分开的;
  2. 不允许「unhandled exception」,所有能处理的异常都必须被处理。

这样一来,Java既避免使用错误感知返回类型时类型混杂的问题,也避免了catch时无法得知到底要catch哪些异常的问题,还避免了异常被忽略的问题。

但是,Java也继承了使用错误感知返回类型的矫枉过正的问题,而且,在Java中无法在两种方式之间自由选择切换。

推荐阅读:

相关文章