作者:HW-HM
上一篇文章【码手防御-5】契约式设计详例,我们通过栈实例来讨论如何进行契约式设计,本篇将介绍异常机制,以及异常使用时的注意事项。
先看一段程序来了解下异常的处理过程。
[code lang='cpp']
#include 'stdafx.h'
class A
{
public:
A() {
printf('Construct A\n');
}
~A() {
printf('Destruct A\n');
//throw('A');
}
};
class B
{
public:
B() {
printf('Construct B\n');
throw('B');
}
~B(){
printf('Destruct B\n');
}
};
int main()
{
//throw('C');
try {
A a;
B b;
printf('After throw\n');
}
catch(char e) {
printf('Catch expection %c\n', e);
}
return 0;
}
[/code]
在类B的构造函数中抛出了异常(字符B),try块中定义了A、B的临时变量,catch块打印出捕获到的异常字符。VC运行结果如下:
Construct A
Construct B
Destruct A
Catch expection B
从中可以看出“After throw”没有输出,因为try块中抛出异常之后的代码不会执行。A的析构函数执行了而B的析构函数没有执行,这是因为B的构造函数中抛出了异常。如果去掉代码中第一个注释,输出“Destruct A”之后程序就崩溃了,因为在A的析构函数中抛出了异常。如果去掉的是代码中的第二个注释,什么输出也没有,程序直接崩溃了,因为这里的throw没有包含在任何的try中。看到这些“因为”,如果你还是不知道为什么,说明你该了解下异常的处理机制了。
当一个异常被抛出时,异常处理系统从抛出异常的函数内开始向上通过嵌套的函数调用链查找与该异常的catch 子句,如果没找到,默认行为是终止程序。在函数调用链中查找catch 子句的过程被称作栈展开。在栈展开期间,随着函数调用链中的函数在catch 子句的查找期间退出,每个函数执行的动作也被立即结束,函数中的局部对象,如果其构造函数调用成功了,则调用其对应的析构函数。再回过头去看上面的运行结果,相信你就明白那些“因为”了。
码手留意1:别在构造函数和析构函数中抛出异常,除非你在同一地方将它们捕获。
码手留意2:绝对不要在异步事件中使用异常。如使用了信号机制的系统、中断处理程序等。因为这些函数(例如信号处理函数、中断处理函数)是操作系统调用的,没有对应的嵌套的函数调用链,抛出异常可能导致不可预期的后果。
从异常处理过程可以看出,异常打断了程序的正常处理逻辑,跟踪起来比较困难。如果将异常用于程序正常处理过程,丧失的是程序的可读性和可维护性。
码手留意3:切不可将异常用于程序正常处理过程。
如果异常不被捕获,程序会终止,这就是异常的第一个作用:在没有关于错误的上下文信息的情况下,通过抛出异常让上层的调用者决定如何处理。就好比是告诉调用者“我搞不定了,你们去搞定吧”。
异常的另一个作用是让程序逻辑更直观——将正常逻辑与异常(错误处理)逻辑分开。如下两段代码对比一下就看出效果了。
不使用异常的代码:
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log('page deleted');
} else {
logger.log('configKey not deleted');
}
} else {
logger.log('deleteReference from registry failed');
}
} else {
logger.log('delete failed');
return E_ERROR;
}
使用异常的代码:
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
接下来讲讲该如何使用异常。
码手留意4:使用异常来通知调用者发生了不可忽略的错误,只有真正例外(异常)的情况,才使用异常。真正的异常是“对方完全满足了契约,而我方仍然没有如约完成任务”。
异常用于决不应该发生的情况(义务未完成),而断言用于任何情况下都不应该发生的情况(即程序Bug)。
码手留意5:别使用异常来推卸责任,如果能够内部处理,就内部处理。有足够的上下文信息,就应该处理这个错误。
码手留意6:在恰当的抽象层次抛出异常,异常也是程序的接口的一部分。异常的抽象层级和接口的抽象一致。
在高层抛出了底层异常:
class Employee {
public TaxId GetTaxId() throws EOFException {<--
}
...
}
抛出这样的异常才是合适的:
class Employee {
public TaxId GetTaxId() throws EmployeeDataNotAvailable { <--
}
...
}
如果函数捕获到异常,没有足够上下文进行处理,应该抛出该层级的异常(重新解释后的),而不是将原来的异常再次抛出。异常是接口的一部分,在函数定义中申明可能抛出的异常。
码手留意7:在异常消息中包含所有导致异常的信息。这方便日志记录,并助于问题定位。
码手留意8:避免空的catch块。这要么是说try块不应该无缘无故抛出异常,要么是catch块有错误没有处理有效的异常。
码手留意9:了解所用函数库可能抛出的异常。
这对于C++尤其重要,C++从C发展而来,为了兼容,未申明抛出的异常默认是可能抛出任何异常实在是个苦恼的事情,必要时要看说明文档。当然个人认为总是有这个必要。
码手留意10:考虑构建一个集中的异常报告器。报告器提供了关于异常种类、如何处理异常、异常消息格式的知识。
码手留意11:标准化项目对异常的使用。定义基类(日志记录、错误报告)、什么情况本地处理异常、什么情况抛出异常、决定是否使用集中异常报告器等。
码手留意12:考虑异常的替代手段,尽量编写不抛出异常的代码。
也就是说:尽量编写始终满足契约的代码。这通常意味着要仔细考虑程序界面(函数接口定义)。以大小写字母转化函数说明。
不是大写字母就抛出异常:
char tolower(char ch)
{
if (ch >= ‘A’ && ch <= ‘Z’)
throw out_of_range;
return( ch + ‘a’-‘A’);
}
非大写字母就返回本身:
char tolower(char ch)
{
if( ch >= ‘A’ && ch <= ‘Z’)
return( ch + ‘a’-‘A’);
else
return ch;
}
调用者需要的无非是得到一个字母的小写,你觉得哪个用起来更方便?






































苏公安网备 32011402010933号