我提到最近一直为一个项目进行Code Review的工作,从中发现了一些问题,同时也有了一些想法。这次我们来关注一个我们每天都会面对的问题:异常处理。
—异常处理不简单—
个人觉得,异常处理对于程序员来说,尤其是对于那些初级程序员来说,是最为熟悉的同时也是最难掌握的。说它熟悉,因为仅仅就是Try/Catch而已。说它难以掌握,很多开发人员却说不清楚Try/Catch应该置于何处?什么情况下需要对异常进行日志记录?什么情况下需要对异常进行封装?什么情况下需要对异常进行替换?对于捕获的异常,在什么情况下需要将其再次抛出?什么情况下则不需要。总之,异常处理没有我们想象的那么简单。
无论对于何种类型的应用,异常处理都是必不可少的。合理的异常处理应该是场景驱动的,在不同的场景下,采用的异常处理策略往往是不同的。异常处理的策略应该是可配置的,因为应用程序出现怎样的异常往往是不可预测的,现有异常策略的不足往往需要在真正出现某种异常的时候才会体现出来,所以我们需要一种动态可配置的异常处理策略维护方式。
—try/catch要不要用—
1.程序要健壮,必须要设计报错机制。
最古老,也是最常见的,比如:
bool CreateFile( );
//如果创建文件失败就返回false,否则返回true。
这种报错方式,显然不好。因为它没有给出产生错误的具体原因。
2.改进:一个函数或过程,会因为不同的原因产生错误,报错机制必须要把这些错误原因进行区分后,再汇报。
比如:
int CreateFile():
//如果创建成功就返回1.
//如果是因为没有权限,导致失败,返回-1。
//如果是因为文件已经存在,导致失败,返回-2。
//如果是因为创建文件发生超时,导致失败,返回-3。
这样看上去,比【1】要好些,至少指出了比较具体的失败原因,但是,还不够。
3.很多情况下,函数需要把详细的原因,用字符串的方式,返回:
class Result
{
....int State;//同【2】
....string ErrorMessage;//如果失败,这里将给出详细的信息,如果有可能,应该把建议也写上去。
}
Result CreateFile();
//如果创建成功,返回的Result,State为1,ErrorMessage为null。
//如果是因为没有权限,导致失败,返回的Result,State为-1,ErrorMessage为"用户【guest】没有权限在【C:\】这个目录下创建该文件。建议您向管理员申请权限,或者更换具有权限的用户。"。
//如果是因为文件已经存在,导致失败,返回的Result,State为-2,ErrorMessage为"文件【C:\abc.txt】已经存在。如果需要覆盖,请添加参数:arg_overwrite = true"。
//如果是因为创建文件发生超时,导致失败,返回的Result,State为-3,ErrorMessage为"在创建文件时超时,请使用chkdsk检查文件系统是否存在问题。"。
4.我个人推崇上面这种方式,完整,美观。但是这种流程,容易与正常的代码混在一起,不好区分开。
因此,Java、C#等设计了try catch这一种特殊的方式:
void CreateFile()
//如果创建成功就不会抛出异常。
//如果是因为没有权限,导致失败,会抛出AccessException,这个Exception的Msg属性为"用户【guest】没有权限在【C:\】这个目录下创建该文件。建议您向管理员申请权限,或者更换具有权限的用户。"。
//如果是因为文件已经存在,导致失败,会抛出FileExistedException,这个Exception的Msg属性为"文件【C:\abc.txt】已经存在。如果需要覆盖,请添加参数:arg_overwrite = true"。
//如果是因为创建文件发生超时,导致失败,会抛出TimeoutException,这个Exception的Msg属性为"在创建文件时超时,请使用chkdsk检查文件系统是否存在问题。"。
可见,上述机制,实际上是用不同的Exception代替了【3】的State。
这种机制,在外层使用时:
try
{
....CreateFile( "C:\abc.txt" );
}
catch( AccessException e )
{
....//代码进入这里说明发生【没有权限错误】
}
catch( FileExistedException e )
{
....//代码进入这里说明发生【文件已经存在错误】
}
catch( TimeoutException e )
{
....//代码进入这里说明发生【超时错误】
}
对比一下【3】,其实这与【3】本质相同,只是写法不同而已。
综上,我个人喜欢【3】这类面向过程的写法。但很多喜欢面向对象的朋友,估计更喜欢【4】的写法。然而【3】与【4】都一样。这两种机制都是优秀的错误处理机制。
—失败的异常处理—
try
{
....CreateFile( "C:\abc.txt" );
}
catch( Exception e )
{
....//代码进入这里说明发生错误
}
当出错后,只知道它出错了,并不知道是什么原因导致错误。这同【1】。
以及,即使CreateFile是按【4】的规则设计的,但菜鸟在外层是这样使用的:
try
{
....CreateFile( "C:\abc.txt" );
}
catch( Exception e )
{
....//代码进入这里说明发生错误
....throw Exception( "发生错误" )
}
这种情况下,如果这位菜鸟的同事,调用了这段代码,或者用户看到这个错误信息,也只能知道发生了错误,但并不清楚错误的原因。这与【1】是相同的。—异常使用的误区—
第一,对错误进行异常处理。所谓错误就是,这里本来不该发生,你却让它发生了,比如传入空指针,数组越界,除数为零。这种时候正确的做法是加断言,或者什么也不做。处理这种错误是自作多情而不负责任的,你知道他为啥错了你就给他处理?你处理了只是把鸵鸟脑袋埋进沙子,问题依然存在。底层库没必要为上层程序员的逻辑错误擦屁股。
第二,认为异常比返回错误码更安全。异常可以直接跳过多层,它必然不如一层层处理安全。如果不配合自动垃圾回收,跳出多层很难保证内存不泄漏,而有gc的语言也难以避免资源泄露。因此,不建议处理异常直接跳出多层函数。
那么异常就没用了吗?当然不是。异常比错误码能够携带更直观的信息,高级语言应该使用异常代替错误码,同时不触及以上误区,则可使程序更健壮。1、只针对不正常的情况才使用异常
2、不要忽略异常
try {
...
} catch (SomeException e) {
}
—那些情况需要异常处理—
使用者(包括用户、代码库的使用者)所引发的错误,需要通过异常机制来处理。
用户在程序运行时触发所导致的错误,需要异常机制来捕捉和处理。
—异常的目的—
将这个问题传递给调用方解决。
参考文章:
http://taobaofed.org/blog/2015/10/28/try-catch-runing-problem/