《代码之髓》读书笔记之四:异常处理

前言

这本书关于异常处理的这一章是我印象最深的一章,详细讲了异常处理的发展历程,以及其中一些关键的技术点。

摘录

错误处理的方法大体可分为两种:使用返回值使用异常(异常处理)。


错误处理的编写方法大体可分为两种。一种是利用 shippai(可能会出错的代码) 函数的返回值来传达程序出错的信息,函数调用方通过检查返回值来相应地对错误进行处理。另一种是在调用 shippai 函数前设定好错误处理的代码,错误发生时能跳转至相应的错误处理代码。前者至今还在 C 语言等语言中经常使用,后者则被称为异常处理。

出错时把信息写入返回值,接着做返回值检查。与之相类似的有先行定义全局变量接收错误信息的方法,还有在函数调用方定义变量作为引用形参接收错误信息的方法。无论哪种,遵循的都是出错时写入信息然后做检查的思路。 这种方法在 C 语言等众多语言中广泛使用。但是它有两个问题: 遗漏错误 错误处理导致代码可读性下降

PL/I 语言是先定义好出错时的处理操作,再编写可能出错的代码。与这种形式不同的是,Java 等语言是先(用 try{…} 括起来)编写可能出错的代码,然后编写出错时的处理操作。那么这种语句结构是何时、基于什么原因产生的呢?


异常处理的一个发展历程。

一、明确声明命令可能抛出何种异常

1975 年,John Goodenough 在自己的论文 {9[John B. Goodenough,“Exception handling: issues and a proposed notation”, Communications of the ACM, Vol.18 Issue 12, ACM, 1975, pp.683-696 此时把失败称为异常(exception)已经很普遍了。]} 中提出了一种更好的异常处理的方法 。他的观点是这样的:命令有可能会抛出异常,而程序员有可能忘记这种可能性,也可能在不正确的地方编写异常处理或者编写不正确类型的异常处理。为使编译器能够对程序员的错误发出警告,减少这种可能性,需要做到两点。一是明确声明命令可能抛出何种异常,二是需要有将可能出错的操作括起来的语句结构。 John Goodenough 在创作这篇论文时是 SofTech, Inc 的职员,之后担任卡耐基梅隆大学软件工程研究所的最高技术负责人。更多详细资料请参考:http://www.sei.cmu.edu/about/people/profile.cfm?id=goodenough_12984 以这里提议的括起来的语句为基础,现代大部分语言采用了先括起来可能出错的操作,再编写错误处理的语句结构。明确声明命令可能抛出何种异常,这个设计方针在 Java 语言的异常检查中得以继承。

二、引入了异常处理的机制

从 1975 年 Goodenough 的论文发表直到 1977 年,程序设计语言 CLU11 引入了异常处理的机制,追加了置于命令后面的错误处理语句结构 except。CLU 语言从最初就具有用 begin…end 将代码括成块状的功能,这一功能和 except 相结合,就实现了将可能出错的操作括起来再补 充错误处理的代码编写方式。

三、 C++的异常处理

不久后的 1983 年,C++ 语言诞生。针对异常处理的语句结构问题从 1984 年到 1989 年间经历了多次讨论,C++ 语言最终确认追加一种语句结构,把关键字 try 放在那些被括起来的可能出错代码的前面,把关键字 catch 放在捕捉并处理错误的代码块前面 13。按照 C++ 语言设计者斯特劳斯特卢普( Bjarne Stroustrup)的说法,try 只是一个为了方便理解的修饰符 14。

四、 微软的SEH

上世纪 90 年代初,微软公司开始用 C 语言编写新的 Windows 操作系统,这就是 1993 年发布的 Windows NT 3.1。这个版本的制作时,也考虑到需要有便于操作的错误处理机制,于是在操作系统和 C 语言编译器导入了结构化异常处理(Structured Exception Handling,SEH)的概念。结构化异常处理中,除了将可能出错的代码括起来的 __try 和将错误处理的代码括起来的 __except 之外,还有将即使出错也要执行的代码括起来的 __finally。


可追加错误类型和可自主触发出错,这两种功能为现代的异常处理机制所继承,具有重要意义。


使用 throw 这一关键字是因为更易理解的 raise 和 singal 两个关键字已经在标准库作为函数名字占用了”。


关于finally。

C++ 语言中没有 finally。那它是如何表现不管异常是否发生都要执行的代码的呢? C++ 语言中使用了一种名叫 RAII(Resource Acquisition Is Initialization,资源获取即初始化)的技术。比如,在操作打开了就要关闭的文件对象时,定义来操作该对象的类,用构造函数打开,用析构函数关闭 17。

2001 年出现的 D 语言以改良 C++ 语言为目标,反对 RAII 是优雅的这一意见。 打开了就要关闭这样紧密关联的操作,反映在代码上时,如果能放在相近的位置就容易理解多了。基于这一考虑,D 语言中引入了作用域守护(scope guard)的概念。通过使用作用域守护,可以事先定义从某一作用域(如函数)跳出时执行的操作。

D语言 
void abc() 
{ 
    Mutex m = new Mutex; 
    lock(m); // 锁住mutex 
    scope(exit) unlock(m); // 定义作用域结束时的解锁操作 
    foo(); // 执行操作 
}

Python、Ruby、JavaScript的一个例子。

函数调用时参数不足的情况 这里我们列举 Python、Ruby、JavaScript 这三种脚本语言,来比较各种语言分别在什么时候抛出异常。比如,调用一个带有两个参数的函数但只传递一个参数时会发生什么? Python 语言和 Ruby 语言会在函数调用的时刻抛出异常。但是 JavaScript 语言会把缺失的参数当作未定义的特殊值(undefined)继续执行。

Python

def foo(x, y): 
  print x, y 
foo(1) 

结果(异常) 

Traceback (most recent call last): File ""tmp.py"", line 4, in <module> foo(1) TypeError: foo() takes exactly 2 arguments (1 given) 
Ruby 

def foo(x, y) 
    p x, y 
end 

foo 1 

结果(异常) 
tmp.rb:1:in `foo': wrong number of arguments (1 for 2) (ArgumentError) from tmp.rb:5:in `<main>' 
JavaScript 

function foo(x, y){ 
    console.log(x, y); 
} foo(1) 

结果(成功) 1 undefined

以上三种语言的设计者都是具有高超技术能力的程序设计员,即便是他们,就何种情况应该抛出异常也不能达成一致。异常应该在何种情况下使用,何为异常的情况,这些问题是没有正确答案的。


发生错误应该停止操作立刻报告,这一设计思想被称为错误优先(fail first)。


异常传递的问题 大家或许认为这样做是理所当然的。其实关于这个意见并未统一,因为这一设计有一个很大的问题。那就是,即使看到了函数 f 的代码也不知道函数 f 可能会抛出什么异常。有可能是函数 f 调用的另外的函数 g 中抛出的异常传递过来的,也有可能是函数 g 调用的函数 h 抛出的异常。也就是说,如果不看见函数 f 调用的所有的函数代码,就无从得知函数 f 抛出何种异常。万一没有察觉到抛出某种异常的可能性,程序就有可能异常终止。

Goodenough 主张为了避免这一问题,需要明确地声明可能抛出的异常。Java 语言就采用了这一方针。

其他语言中所谓的异常,Java 语言中的 throw 语句也能抛出,并进一步分为三类:不应该做异常处理的重大问题、可做异常处理的运行时异常和可做异常处理的其他异常。这里的其他异常叫做检查型异常,如果在方法之外抛出,就需要在定义方法时声明。

可以说检查型异常是一种非常好的机制。但是这种机制并没有很好地普及到其他语言中,这是为什么呢? 一言以蔽之,就是因为它太麻烦。一旦 throws 或 try/catch 中异常的数目增多,或者某一方法需要追加一种异常,就不得不修改调用了该方法的所有方法,特别麻烦。


程序也会出错以及程序出错后是如何传达错误的。错误传达方法大致有两种,通过返回值传达和出错后跳转。 通过返回值传达的方法有一个问题,那就是容易忘记检查返回值从而出错。因此,长期以来,人们把更多的精力放在研究出错后跳转的方法上,这一方法成为目前被 Java、C++、Python、Ruby 等众多语言支持的异常处理机制的基础。 异常处理也有几个问题。一个是当函数有不只一个出口时,必须成对处理的操作很难正确地成对处理。另一个是即便看了代码也不知道函数将抛出何种异常。

Java语言的开发者为了解决第二个问题导入了检查型异常,但是这种方法并不太被接受。C# 语言的开发者一方面承认检查型异常的优势,另一方面希望有更好的方法出现。


2016-11-03 22:19:00 hzct


原创文章,转载请注明: 转载自赵德栋的 博客

个人主页 CSDN博客

作者:赵德栋,作者介绍

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值