当前位置:嗨网首页>书籍在线阅读

02-异常处理简介

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

6.1 异常处理简介

“从那时起,当计算机出现任何问题时,我们都说它里面有bug。”

——Grace Hopper

编写预期条件下表现良好的程序是一个好的开始,但当程序遇到意外情况时,这就会变得非常具有挑战性。正确的异常处理是软件开发中重要但又经常被忽略的一个方面。大多数异常处理一般有3种。

  • 可恢复异常是用户和环境与程序交互时预期会发生的异常。例如文件未找到(file not found)或数字解析错误。
  • 不可恢复异常是违反契约或程序常量的异常,例如索引越界或除以零的操作(分母是零)。
  • 致命性异常是立即让程序终止运行的异常。这种情况包括内存不足和堆栈溢出。

在实际应用中的程序通常需要处理错误。例如Web应用程序的恶意输入、网络客户端中的连接故障、文件系统损坏,以及数字化应用程序中的整数溢出错误。如果没有错误处理,程序在遇到意外情况时就会崩溃或者被操作系统终止。大多数情况下,这不是我们希望程序在出现意外时应该表现出的行为。例如,考虑一个实时流处理服务,由于发送格式错误,客户端信息解析失败,该服务在某个时间无法接收来自客户端的信息,如果我们无法处理这个问题,那么服务将在每次解析错误时终止运行。从可用性的角度来看这是很糟糕的,并且绝对不应该是网络应用程序应该有的特征。服务处理这类错误的理想方式是捕获错误,对其进行操作,然后将错误日志传递给日志聚合服务以供后续分析,并继续从其他客户端接收消息。这就构造了处理可恢复错误的方法,通常这也是模拟错误处理的实用方法。在这种情况下,语言的错误处理结构能够帮助程序员拦截错误,并对它们采取相应的措施,从而避免程序被终止运行。

关于错误处理的两种非常流行的范式是返回代码和异常。C语言中包含返回代码模型。这是一种非常简单的错误处理形式,其中函数使用整数作为返回值来表示操作成功与否。在发生错误时,大量的C函数会返回−1或NULL。当进行系统调用时出现错误,C语言会设置全局变量errno表示调用失败。但是作为一个全局变量,是无法阻止用户在程序的任意位置对它进行修改的,仍然需要程序员检查此错误值并处理它。通常,这种方式会变得非常“神秘”并容易出错,因此并不是一种灵活的解决方案。除非使用静态分析工具,否则如果我们忘记检查返回值,那么编译器将不会向我们发送警告。

另一种处理错误的方法是通过异常来实现的。诸如Java和C#之类的高级编程语言就是采用这种方式处理错误的。在这种范式中,可能出错的代码应该包含在try代码块中,try代码块中的任何错误都必须在catch代码块中捕获。(理想情况下,在try代码块之后就应该立即使用catch代码块。)但是异常也有其不足,抛出异常的开销是很昂贵的,因为程序必须展开堆栈,找到适当的异常处理程序,并运行相关的代码。为了避免这种开销,程序员经常采用防御性代码的形式检查抛出异常的代码,再继续执行其他操作。此外,异常的实现在许多语言中都存在缺陷,因为它允许新手使用基础异常类(例如Java中的throwable)捕获所有代码块的异常,如果它们只是记录并忽略该异常,那么可能导致程序中的状态不一致。此外,在这些语言中,程序员无法通过查看代码来了解方法是否可以抛出异常,除非他们使用带有检查异常的方法。这使得程序员很难编写出安全的代码。因此,开发者经常需要依赖方法的说明文档(如果有的话)来确定是否可以抛出异常。

另一方面,Rust包含基于类型的错误处理方式,这在函数式编程语言中是很常见的(例如OCaml和Haskell),同时它类似于C语言的返回错误代码模型。但是在Rust中,返回值表示适当的异常类型,并且支持用户自定义类型。语言的类型系统要求在编译时处理错误状态。如果你了解Haskell,这与它的Maybe和Either类型相似,Rust只是提供的名称不同,即可恢复错误的Option和Result类型。对于不可恢复错误,有一种被称为panic的机制,它是一种灾难性故障处理机制,当程序中存在错误或破坏常量的情况时,建议将其用作最后的处理措施。

Rust为什么要选择这种错误处理方式呢? 如前所述,异常及其相关的堆栈展开会产生开销。这与Rust的零运行时成本的核心理念相悖。其次,异常式的错误处理(通常是其典型实现)允许通过catch-all异常处理程序忽略这些错误。这可能会造成程序状态的不一致,违背Rust的安全性原则。

接下来,让我们深入研究一下可恢复异常。