昨晚翻了翻《松本行弘的程序世界》这本书,看到他对异常设计原则的讲述,觉得颇为赞同。近期的面试,我有时也问类似的问题,但应聘者的回答大都不能令人满意。有必要理一理,说说我是怎么理解的,以及在编程实践中如何做出合适的选择。当然这只是一家之言,未必就是完全正确的。
首先,要明确一点的是,错误码和异常,这两者在程序的表达能力上是等价的。它们都可以向调用者传达“与常规情况不一样的状态”。考虑使用哪一种,更多地是从API的设计、系统的性能指标、新旧代码的一致性这3个角度来考虑的(本文主要从API设计的角度着手)。
接下来,看一个使用返回错误码的例子:
#include
using namespace std;
int strlen(char *string) {
if (string == NULL) {
return -1;
}
int len = 0;
while(*string++ != \'\\0\') {
len += 1;
}
return len;
}
int main(void) {
int rc;
char input[] = {0};
rc = strlen(input);
if (rc == -1) {
cout << \"Error input!\" << endl;
return -1;
}
cout << \"strlen: \" << rc << endl;
char *input2 = NULL;
rc = strlen(input2);
if (rc == -1) {
cout << \"Error input!\" << endl;
return -2;
}
cout << \"strlen: \" << rc << endl;
return 0;
}
与之等价的使用异常的程序是:
#include
using namespace std;
int strlen(char *string) {
if (string == NULL) {
throw \"Invalid input!\";
}
int len = 0;
while(*string++ != \'\\0\') {
len += 1;
}
return len;
}
int main(void) {
char input[] = {0};
cout << \"strlen: \" << strlen(input) << endl;
char *input2 = NULL;
cout << \"strlen: \" << strlen(input2) << endl;
return 0;
}
从以上两个程序片段的对比中,不难看出使用异常的程序更为简洁易懂。为什么?
原因是:返回错误码的方式,使得调用方必须对返回值进行判断,并作相应的处理。这里的处理行为,大部份情况下只是打一下日志,然后返回,如此这般一直传递到最上层的调用方,由它终止本次的调用行为。这里强调的是,“必须要处理错误码“,否则会有两个问题:1)程序接下来的行为都是基于不确定的状态,继续往下执行的话就有可能隐藏BUG;2)自下而上传递的过程实际上是语言系统出栈的过程,我们必须在每一层都记下日志以形成日志栈,这样才便于追查问题。
而采用异常的方式,只管写出常规情况下的逻辑就可以了,一旦出现异常情况,语言系统会接管自下而上传递信息的过程。我们不用在每一层调用都进行判断处理(不明确处理,语言系统自动向上传播)。最上层的调用方很容易就可以获得本次的调用栈,把该调用栈记录下来就可以了。因此,使用异常能够提供更为简洁的API。
上述的例子还不是最绝的,因为错误码和常规输出值并没有交集,那最绝的情况是什么呢?错误码侵入了或者说污染了常规输出值的值域了,这时只能通过其它的渠道返回常规输出了。如:
#include
using namespace std;
int get_avg_temperature(int day, int *result) {
if (day < 0) {
return -1;
}
*result = day;
return 0;
}
int main(void) {
int rc;
int result;
rc = get_avg_temperature(1, &result);
if (rc == -1) {
cout << \"Error input!\" << endl;
return -1;
}
cout << \"avg temperature: \" << result << endl;
rc = get_avg_temperature(-1, &result);
if (rc == -1) {
cout << \"Error input!\" << endl;
return -2;
}
cout << \"avg temperature: \" << result << endl;
return 0;
}
当然,如果能忍受低效率,也可以把错误码和常规输出捆到一个结构里再返回,如下:
#include
using namespace std;
typedef struct {
int rc;
int result;
} box_t;
box_t get_avg_temperature(int day) {
box_t b;
if (day < 0) {
b.rc = -1;
b.result = 0;
return b;
}
b.rc = day;
b.result = 0;
return b;
}
int main(void) {
box_t b;
b = get_avg_temperature(1);
if (b.rc == -1) {
cout << \"Error input!\" << endl;
return -1;
}
cout << \"avg temperature: \" << b.result << endl;
b = get_avg_temperature(-1);
if (b.rc == -1) {
cout << \"Error input!\" << endl;
return -2;
}
cout << \"avg temperature: \" << b.result << endl;
return 0;
}
与之等价的使用异常的程序是:
#include
using namespace std;
int get_avg_temperature(int day) {
if (day < 0) {
throw \"Invalid day!\";
}
return day;
}
int main(void) {
cout << \"avg temperature: \" << get_avg_temperature(1) << endl;
cout << \"avg temperature: \" << get_avg_temperature(-1) << endl;
return 0;
}
哪一个丑陋,哪一个优雅,我想应该不用我多说了。既然使用异常这么好,那我们是不是干脆全部使用异常算了?当然也不是。以下这个例子也使用了异常,但我们可以看到程序仍然比较冗长:
#include
#include
#include
using namespace std;
class database {
private:
map store;
public:
database() {
store[\"a\"] = 100;
store[\"b\"] = 99;
store[\"c\"] = 98;
}
int get(string key) {
map::iterator iter = store.find(key);
if (iter == store.end()) {
throw \"No such user exception!\";
}
return iter->second;
}
};
int main(void) {
database db;
try {
cout << \"score: \" << db.get(\"a\") << endl;
} catch (char const *&e) {
cout << e << endl;
}
try {
cout << \"score: \" << db.get(\"d\") << endl;
} catch (char const *&e) {
cout << e << endl;
}
return 0;
}
与之等价的使用错误码的程序如下:
#include
#include
#include
using namespace std;
class database {
private:
map store;
public:
database() {
store[\"a\"] = 100;
store[\"b\"] = 99;
store[\"c\"] = 98;
}
map::iterator get(string key) {
return store.find(key);
}
inline map::iterator not_exist() {
return store.end();
}
};
int main(void) {
database db;
map::iterator iter;
iter = db.get(\"a\");
if (iter == db.not_exist()) {
cout << \"no such user!\" << endl;
} else {
cout << \"score: \" << iter->second << endl;
}
iter = db.get(\"d\");
if (iter == db.not_exist()) {
cout << \"no such user!\" << endl;
} else {
cout << \"score: \" << iter->second << endl;
}
return 0;
}
在这个例子当中,使用异常并没有带来明显的好处,该做的事情还得做。这种情况下,是选择错误码还是选择异常要结合系统自身的需求和现有代码的情况了,当然,有时候这纯粹是个人口味问题。接下来再举一些例子:
使用错误码的例子:
1、检索数据时,对应某一键不存在相应的记录的情况。这时应使用错误码。这种情况要明确告诉调用方,并不是系统出异常了,而是数据确实不存在,请作出相应的处理。
2、查找用户时,输入的用户名并不存在的情况。这时应使用错误码。这种情况要明确告诉调用方,并不是系统出异常了,而是用户确实不存在,请作出相应的处理。
使用异常的例子:
1、读取文件时,文件不存在的情况。(我要读取文件,我认为它是存在的(可能已经进行了相应的判断),但现在文件不存在了(可能中途被人删除了),OK,抛出异常,调用方不明确处理的话,系统异常终止)。
2、修改用户资料时,用户不存在的情况。(我要修改用户资料,我认为用户是存在的(可能已经进行了相应的判断),但现在用户不存在了(可能中途被人删除了),OK,抛出异常,调用方不明确处理的话,系统异常终止)。
模凌两可的例子:
1、入栈,栈满;出栈,栈空。
2、数组越界。
3、除0错。
4、连接时,网络出错。
综上所述,是返回错误码还是抛出异常,有以下3条规则:
规则1:本次发生的异常现象是不是真的非常罕见?非常罕见意味着发生该现象的机率非常低,调用方每次都处理将浪费大量的精力,但如果出现,系统应该明确终止,而不是继续往下执行,所以使用异常;其它情况,参考规则2。
规则2:本次发生的异常现象是不是出乎意料之外?出乎意料之外意味着系统已经出问题了(状态和预期不一致),被调用方没办法再往下执行了,而调用方如果不明确处理的话,系统也应该终止(因为系统状态出问题了),所以使用异常;其它情况,参考规则3。
规则3:遇到模凌两可的情况,则根据系统的性能需求(异常使用不当可能会造成系统抖动)、现有代码的情况(要保持一致的程序风格)以及开发人员的口味(怎么舒服怎么来)。
原文