- 论坛徽章:
- 0
|
我之异常观
经常在论坛里看人们争论,该不该使用异常、何时使用异常,于是把自己的一些心得整理出来,欢迎大家拍砖。
注释:以下用Q表示Question,A表示Short Answer;另外,本文中的代码使用的是类似C++的格式。
Q:该不该使用异常?
A:应该使用异常。
解释:1)既然你所使用的语言提供了异常的机制,而且在几乎所有的(我所知道的)面向对象语言中,异常都是语言的一个重要部分,而不是一个可有可无、仅仅用来吸引使用者的“小甜头”,那么就应该使用这种机制。
换言之,相信你所使用语言的“XXX之父”,相信他们在异常这个问题上的理解,要比你自己要深入。
2)异常确实改善了代码的可读性。比较下面两段代码:
a:
if(fun1(a,b,c)!=0){
error_log("some error");
}
if(fun2(e,f,g)!=0){
error_log("some error");
}
b:
try{
fun1(a,b,c);
fun2(e,f,g);
}
catch(Exception e){
error_log("some error");
}
代码a将代码的正常执行和异常处理混杂在一起;在看别人代码的时候,碰到异常处理的地方,如非必要,我一般都是直接跳过的,你呢?
另外,我记得有哪位高人提倡过,一个函数的长度,应该尽量保持在一个屏幕能显示的高度以内。一般情况下,代码a都会比代码b要长。
当然,你发觉了,代码a里面进行了两次捕捉,而代码b只有一次。
这不公平,你会说。
不过,a)异常也可以有继承体系,如果fun1是抛出网络A异常,fun2是抛出网络B异常,那么我们也许捕捉网络异常这个“父异常”就够了,不需要区分究竟是哪种网络异常。
b)你并不需要频繁的捕捉异常,在后面你会看到。
3)使用特定返回值的方法污染了返回值的正常范围:
if ((ret=func()) == 0) //这是不需要正常返回值或返回值为正整数的情况
if ((ret=func()) < 0) //这是需要返回正整数或0的情况
if (func(&ret) == 0) //呼呼,需要返回整数范围,返回值被挤到函数参数里边去了。
另外,这样还仅仅是指示发生了异常,但是还不知道发生了什么异常;想知道,找errno或GetLastError去。
4)效率,运行效率,你会说。
我知道,异常会有时间和空间的开销。
但是,你确认,你真的需要去发掘/节省异常的开销?你确认你的程序没有在等待网络、没有在等待文件读取、没有在无谓的等待?确认程序的逻辑正确、运行合理,要远远胜过挖掘这一点语言的潜力。
如果,如果你真的需要这样做,那么也许你根本就不应该选择C++(更遑论java)。即使使用了C++,也需要使用一个严格界定的子集,这时候别说异常,可能所有的C++库,甚至连继承等等特性能不能使用,都需要考察了,也许你真的需要的是C。
5)是否使用异常,有的时候不取决于你。你使用的库抛出了异常,你也只好捕捉它。
Q:怎样使用异常?
A:异常的使用包括抛出和捕捉两个方面:
1)尽可能地抛出异常。
2)仅在需要时捕捉异常。
解释:尽可能的抛出异常很容易理解,你的代码里边出现了预料不到的情况,就应该作为异常抛出。
仅在需要时捕捉,也可以表述为:如果没有必要,那么就不要捕捉它。
与之相对应的,是异常的滥用----层层捕捉异常,层层抛出异常。比如:
void fun1(){
try{
some_fun();
}
catch(SomeException){
throw MyException1();
}
}
void fun2(){
try{
other_fun();
}
catch(OtherException){
throw MyException2();
}
}
void fun3(){
try{
fun1();
fun2()
}
catch(SomeException,OtherException){
throw MyException3();
}
}
void main(){
try{
fun3();
}
catch(MyException3){
...
}
}
而理想的异常捕捉方式是:
void fun1(){
some_fun();
}
void fun2(){
other_fun();
}
void fun3(){
fun1();
fun2();
}
void main(){
try{
fun3();
}
catch(SomeException){
...
}
catch(OtherException){
...
}
}
对比两段代码,可以看到:
1、上面的那段代码非常丑陋。
2、上面的那段代码不必要的引入了三个异常类别:MyException1、MyException2、MyException3。
3、最重要的,上面的那段代码丢失了异常信息。在main函数里边,已经无从区别到底是some_fun里触发的异常,还是other_fun触发的异常。而要避免信息的丢失,就需要在每个调用层次定义、捕捉、重新抛出同样数量的异常。设想一下具有10个调用层次,10个底层的异常传递到顶层,就需要额外定义、捕捉、抛出90个异常,这种重复毫无意义。
4、下面的代码更容易调试。推荐为调试写不带任何异常捕捉的测试代码,这样出错的时候可以直接定位到出错位置。
需要捕捉异常的场合:
1、具有图形界面的程序,应该在每个功能的调用点捕捉异常,根据异常类别给出明确的错误信息。这样的程序用户会接受;程序的内部运行信息对最终用户毫无用处,而且招致用户的反感。
void button1_click(void){
try{
do_something();
do_otherthing();
}
catch(SomeException){
show_msg("Some exception");
}
catch(OtherException){
show_msg("OtherException");
}
}
2、需要不间断重复运行的程序,比如系统服务程序,你当然不希望遇到一点意外程序就关闭。所以在重复处理的循环里面,捕捉异常并记录日志,然后开始下次重复执行。
while (1){
try{
do();
}
catch(Exception e){
log(e);
}
} |
|