线索内还是集中式的错误处理?

这两个概念(Inline Error Handling 和 Centralized Error Handling)是我在用 VB 编写程序的时候遇到的。这两个概念分别是逐行错误处理和集中式错误处理。为什么会有这两个问题呢?是这样的:BASIC 原先有两种错误处理方式,一种是 ON ERROR GOTO XXX,一种是 ON ERROR RESUME NEXT。这两个错误处理方式不一样,表现在 ON ERROR GOTO XXX 表示在这之后所有的错误都放在 XXX 位置处处理;ON ERROR RESUME NEXT 则表示在错误发生之后立刻检查错误代码。

我以前高中编 BASIC 程序的时候,编写的程序都很简单,里面没有考虑什么错误处理。然后在大学学习 C 语言编程的时候,都是用逐行错误处理的。为什么呢?因为 C 语言的机制中,除了 RUN-TIME ERROR(运行时错误)之外,都是用函数的返回值作为错误信息的。那么返回值当然要在调用的这一行上捕获,所以是逐行式的错误处理。然后在学习 C++ 的时候,虽然听说 C++ 有集中式错误处理功能,也就是 Exception(异常,或者叫“例外”)机制,但是没有怎么用过,编写 WINDOWS 程序的时候 MFC 也较少用这种错误处理方式,以至于我根本没有遇到 MFC 中的异常处理。所以可以说,一直以来我都是用的逐行式的错误处理机制,而没有用过集中式错误处理机制。现在使用 Java 和 C# 开始编程了,发现里面有很多地方都是采用异常来处理错误的。那么必然要用到集中式错误处理机制。所以我想了解一下这两种机制有什么区别和各有什么优缺点。

逐行式错误处理机制给我的第一印象就是易懂。如果不易懂,我也没有那么快就会用上它。它其实是很简单的思想:用函数的返回值来告诉调用者这个函数在调用的时候是不是出了错误。如果出了错误,那么再做相应的处理。所以写起来就是先调用一个函数,然后获取它的返回值,再判断这个返回值是多少。如果是出错了,那么再处理它。

集中式错误处理机制,在学 C++ 的时候第一次注意到它(在学 BASIC 的时候早就见过,不过想都没想过,也没有用过),里面是一个很不错的思想,但是我却没有好好领会,而将它放在了一边。主要因为我的习惯思想,不想放弃以前用惯的错误处理机制。这种集中式错误处理机制有什么好处呢?是这样的:在使用逐行错误处理的时候,很多情况下,函数如果捕获一个错误,通常都是不作处理的,而是返回另一个错误。如果这个函数是和用户作交互的,那么这个函数可能就向用户发出一条消息表示它的执行过程中发生了错误。而集中错误处理的时候,比如说抛出一个异常,这个异常会在最接近的地方被捕获。比如说,如果它在当前函数的一个 try 块中,而且相应的 catch 块可以接受这个错误,那么它就会在这个 catch 块中被捕获。如果在这里没有被捕获,也会在最近的一个满足条件的调用者函数那里被捕获(这个机制在编译器那里怎样实现,我还不知道,将来如果有机会就研究一下)。

但是,从上面举的例子中可以看出:一般这些异常都在某个合适的地方被捕获。比如说,一个函数抛出一个除零异常,而这个函数是做除法的,那么调用它的函数是否应该捕获这个异常,取决于它的功能定义。如果调用方期望出现错误,那么应该由除法函数捕获后以一个特殊返回值的形式通知调用方。如果调用方不期望出现错误,那么应该由调用方先进行参数检查后再调用除法函数,并且不捕获异常。由于参数经过了检查,理论上不会出现异常。一旦出现异常,将会终止整个程序。这样,期望出现的错误,在靠里面的地方捕获。不期望出现的错误,在靠外面的地方捕获。结果就能让程序既满足需求又不过于复杂。

最后,作一下结论。从上面的讨论可以看出,在大多数情况下,集中式错误处理和逐行式错误处理只有两个地方不同:一是集中式错误处理可以让程序的逻辑结构显得更清晰;二是集中式错误处理机制可以让没有得到处理的错误在最外层被编译器捕获然后显示出来,而不是隐没在程序里面。这样对调试程序更有好处,也让编程更规范。

集中式错误处理机制中如果要试图恢复怎么办呢?就是将 try catch 块放在一个循环中。这是 Thinking In Java 中提到的。不过具体的实现,应该考虑编写“异常安全代码”。关于这个话题,请参考相关文章。

另外,finally 是干什么的呢?就是让最后要做的固有的恢复工作都放在一起。在 C++ 中,对象的析构,内存的释放,都是可能的事务。在 Java 中,虽然没有这两种事务,但是也可以做一些其他事情,比如说对 light l 调用 l.off() 等。使用 finally 和放在 try catch 块之后的区别是什么呢?如果有一个错误没有被捕获,那么就要循函数栈向上寻找捕获的地方,这个时候 try catch 块之后的代码就无法得到执行而 finally 块就可以得到执行。

注意到,实际上标准 C++ 语言中是没有 finally 关键字的。这是为什么呢?因为 C++ 语言鼓励程序员使用析构函数,通过编译器的栈开解(stack unwinding),来自动地释放资源。但是说起来容易,写起来并非如此。一定要考虑到每个被创建的对象都能被安全地释放的问题。

最后,Thinking In Java 第二版里面说到当时 Java 的一个缺憾:在 try 中发生了一个异常,在 finally 中如果再产生一个异常,这个异常就无法被捕获,因为已经有一个异常等待捕获了。在 C++ 语言中还有另一种可能的情况:在 try 中产生一个异常而导致函数退出的时候,要执行对函数中定义的对象的析构,那么这个过程中也有可能产生新的异常。这些都是异常处理中比较让人不快的事情。当然逐行式错误处理遇到这种问题也很头大。

返回“编程天地”