对异常处理的三种方法比较

当调用某个函数、在函数中进行参数验证或对动态分配的内存进行处理主要有三种手段:
1) 硬编码(hard code)
2) 使用断言(Assert)
3) 使用异常(Exception)
这三种方法各有好处,在不同的场合可以使用不同的方法,可以根据侧重点的不同选择适合的方法进行异常处理。下面以对一个链表处理函数的参数验证为例,讨论并比较三种方法。

假设有一个函数要处理一个链表。

bool ProcessLinkedList(LinkedList* list)
{
// check whether list is NULL
// process list here
return true;
}

1. 硬编码(Hard code)

就是使用程序直接编码进行处理参数检验,上例中对参数list的检查可以这样

if( list == NULL){
return false;
}

这样有一个好处,就是保证调试版本与发行版本的行为一致性,因为无论是在发行版本还是调试版本,它们的代码都是一样的,保证对参数始终被检查,保证不会因为调用空指针而使程序崩溃。同样这样做也有一些缺点:1) 影响程序效率:因为这些代码对每一次凋用都要进行检查操作,在调试版本中倒问题不大,但如果放到发行版本里面,如果函数调用频繁会影响程序的效率;2) 问题定位困难:如果这个函数是很多层函数调用才调用到的,而且这些函数调用检查都是通过返回值进行检查的,那么要定位错误发生的根源必须逐层查找,相当困难(使用异常处理就可以解决这个问题)。

2. 使用断言(Assert)

使用断言,就可以使用assert(exp)函数,一般编译器都支持这个函数,确切地说是一个宏,在调试版本中该宏会对参数进行检查,如果参数的表达式结果为false则会输出错误信息并终止程序(调用abort()函数)。上例中的参数检查可以这样使用assert:

assert(list != NULL);

这样有个好处,就是在调试版本中启用参数检查,而在发行版本中可以禁用该宏,该行代码便被一空行代替,这样可以提高运行版本的程序,但这又引出另外一个问题--既然去掉的参数检查,则会使程序变得不健壮、不可靠。特别是对一些处理用户输入的程序,这种检查是绝对必要的。俗话说“人上一百形形色色”,用户的输入也一样,什么样的输入都有可能被传给程序,所以对程序的输入进行检查,滤掉不期望的输入是必要的,既然在调试版本中有检查,为什么要在发行版本中去掉呢?Arnold Robbins在它的<>中有一个比喻很有意思:What would we think of a sailing enthusiast who wears his lifejacket when training on dray land, but takes it off as soon as he goes to sea?这个比较非常恰切,学习的时候带着救生圈,真正下海的时候丢掉,真的就不需要了吗?

虽然,对禁用断言的做法表示惯于怀疑,但它至少给了我们一种选择,如果对于性能要求非常高的应用程序,比如驱动程序、网络协议栈,来说这也是有意义的,它给这种特殊情况带来了一种比较好的调试方案。

实际上使用断言这情况代表了一种更广义的手段--条件编译。assert本身是个宏而不是一个函数,所以这与自行编写一个函数进行一些输出是一样的,或者加一些条件编译预处理指令,这属于一大类,只是assert将条件编译进行了一次“标准化“。

3.使用异常(Exception)

除了以上两种方式,对于支持异常的语言,如C++/C#/Java,来说这是一种不错的选择,例如(C++)上例的验证可以使用以下代码:

if( NULL == list){
throw invalid_argument("Invalid argument list of ProcessLinkedList( )!");
}

这种方式是一种值得推荐的方法,因为它具有更多的优点:
1)处理灵活。在后台(底层)代码抛出异常后,可以在UI中进行异常处理,是通过GUI显示消息还是通过控制台显示文本信息,甚至可以使用远程终端将异常消息传给远程客户;对于可忽略的异常,可以在捕捉到以后不做任何处理。
2)异常传递。不论代码通过多少层函数调用调用了该函数,如果中途没有进行异常捕获,那么它会被一直传递到最上层调用,您的代码可以选择在任何你想处理它的地方把它捕获到并进行相应处理。
3)快速定位。通过该异常类型以及提供的消息,不论是在函数调用的哪一层捕捉到该异常,您都可以快速进定位该异常,很快找到问题之所在。
4)结构化异常处理。对于c#/Java等支持结构化异常处理的编程语言,很容易实现结构化异常处理,使代码的健壮性更好,而对于C++,VC也进行了扩展,可以进行结构化异常处理。

注意,如果使用C++抛出异常,如果注意,千万不要使用

throw new exception();

如果你使用new创建了exception对象,你将无法将其回收,造成内存泄漏,如果调用频繁,您有多少内存也不够它吃。

上面讨论的三种异常处理方式各有优点,对于不同的应场合各有用武之地。当然,天下没有免费的午餐,不论你使用哪种方法进行了异常处理,都会对性能产生一定的影响。其中异常,是优点最多,也是消耗最大的一种方法。首先,从编码上讲,如果您想根据你的应用程序对不同情况使用不同的异常类型,那么您可能需要自己实现一些异常类,需要较多的编码工作;再者,上旦发生异常,异常对象的构造是需要消耗内存及CPU资源的,而且C++中的异常在捕捉后都会进行复制操作,又一次增加了其消耗。谈到异常的消耗是对性能极其敏感的底层应用而言的,对于一般的应用,强列建议使用异常进行异常情况处理。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭