昨晚翻了翻《松本行弘的程序世界》这本书,看到他对异常设计原则的讲述,觉得颇为赞同。近期的面试,我有时也问类似的问题,但应聘者的回答大都不能令人满意。有必要理一理,说说我是怎么理解的,以及在编程实践中如何做出合适的选择。当然这只是一家之言,未必就是完全正确的。
在行文之前,我有一个观点需要明确:错误码和异常,这两者在程序的表达能力上是等价的,它们都可以向调用者传达“与常规情况不一样的状态”。因此,要使用哪一种,是需要从API的设计、系统的性能指标、新旧代码的一致性这3个角度来考虑的。本文主要从API的设计着手,试图解决两个问题:1)为什么要使用异常?2)什么时候应返回特殊值(注:这不是错误码)而不是抛出异常?
好,先来看一个使用返回错误码的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include <iostream>
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 << "String length: " << rc << endl;
char *input2 = NULL;
rc = strlen(input2);
if (rc == -1) {
cout << "Error input!" << endl;
return -2;
}
cout << "String length: " << rc << endl;
return 0;
}
|
与之等价的使用异常的程序是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#include <iostream>
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 << "String length: " << strlen(input) << endl;
char *input2 = NULL;
cout << "String length: " << strlen(input2) << endl;
return 0;
}
|
从以上两个程序片段的对比中,不难看出使用异常的程序更为简洁易懂。为什么?
原因是:返回错误码的方式,使得调用方必须对返回值进行判断,并作相应的处理。这里的处理行为,大部份情况下只是打一下日志,然后返回,如此这般一直传递到最上层的调用方,由它终止本次的调用行为。这里强调的是,“必须要处理错误码“,否则会有两个问题:1)程序接下来的行为都是基于不确定的状态,继续往下执行的话就有可能隐藏BUG;2)自下而上传递的过程实际上是语言系统出栈的过程,我们必须在每一层都记下日志以形成日志栈,这样才便于追查问题。
而采用异常的方式,只管写出常规情况下的逻辑就可以了,一旦出现异常情况,语言系统会接管自下而上传递信息的过程。我们不用在每一层调用都进行判断处理(不明确处理,语言系统自动向上传播)。最上层的调用方很容易就可以获得本次的调用栈,把该调用栈记录下来就可以了。因此,使用异常能够提供更为简洁的API。
上述的例子还不是最绝的,因为错误码和常规输出值并没有交集,那最绝的情况是什么呢?错误码侵入了或者说污染了常规输出值的值域了,这时只能通过其它的渠道返回常规输出了。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#include <iostream>
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;
}
|
当然,如果能忍受低效率,也可以把错误码和常规输出捆到一个结构里再返回,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include <iostream>
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;
}
|
与之等价的使用异常的程序是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <iostream>
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;
}
|
哪一个丑陋,哪一个优雅,我想应该不用我多说了。异常机制虽好,但要是使用不当,设计出来的API反而会比较难用。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
#include <iostream>
#include <string>
#include <map>
using namespace std;
class database {
private:
map<string, int> store;
public:
database() {
store["a"] = 100;
store["b"] = 99;
store["c"] = 98;
}
int get(string key) {
map<string, int>::iterator iter = store.find(key);
if (iter == store.end()) {
throw "No such user!";
}
return iter->second;
}
};
int main(void) {
database db;
try {
cout << "Score: " << db.get("a") << endl;
} catch (char const *&e) {
cout << "No such user!" << endl;
} catch (...) {
cout << e << endl;
}
try {
cout << "Score: " << db.get("d") << endl;
} catch (char const *&e) {
cout << "No such user!" << endl;
} catch (...) {
cout << e << endl;
}
return 0;
}
|
这个例子也使用了异常,但却是不恰当的使用。因为,“找”这个操作只有两个结果:要么“找到”,要么“没找到”。换句话说,“没找到“也是一种常规输出值。一旦抛出常规输出值,那在调用链上的所有层次里都需要捕获该异常并进行处理,那么使用异常的初衷和好处也就消失了。实践中,在这种查找类的功能里,如果没找到相应记录,一般是通过返回一个特殊的值来告知调用方,比如:NULL、特殊的对象(如iterator)、特殊的整数(如EOF)等等(为什么?一是使用异常没带来什么好处,二是逻辑统一可能为后续处理带来便利)。因此,上述例子可以改造为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include <iostream>
#include <string>
#include <map>
using namespace std;
class database {
private:
map<string, int> store;
public:
database() {
store["a"] = 100;
store["b"] = 99;
store["c"] = 98;
}
map<string, int>::iterator get(string key) {
return store.find(key);
}
inline map<string, int>::iterator end_iterator() {
return store.end();
}
};
int main(void) {
database db;
map<string, int>::iterator iter;
iter = db.get("a");
if (iter == db.end_iterator()) {
cout << "No such user!" << endl;
} else {
cout << "Score: " << iter->second << endl;
}
iter = db.get("d");
if (iter == db.end_iterator()) {
cout << "No such user!" << endl;
} else {
cout << "Score: " << iter->second << endl;
}
return 0;
}
|
接下来再举一些例子:
使用特殊值的例子:
1、检索数据时,对应某一键不存在相应的记录的情况。
2、判断是与否。
使用异常的例子:
1、读取文件时,文件不存在的情况。
2、修改用户资料时,用户不存在的情况。
3、参数出错。
4、数组越界。
5、除0错。
6、入栈,栈满;出栈,栈空。
7、网络错误。
综上所述,本文的结论是:
1、异常能提供更为简洁的API,并且能更早地发现隐藏的BUG。如有可能,要尽量采用。
2、不要抛出原本属于返回值值域里的值,一般是直接返回特殊值。经典使用场景是查找和判断。
—The end.
相关推荐
"关于异常的作业"这一主题,通常涉及到如何有效地捕获、处理和预防程序运行时可能出现的错误情况。在这个作业中,我们将深入探讨异常的概念、异常处理机制以及在不同编程语言中的实现方式。 异常是在程序执行过程中...
关于异常的使用心得_1
在机器学习领域,异常检测是一种重要的技术,常用于识别数据中的异常值或离群点。这个数据集专门针对异常检测,对于理解机器学习算法在处理此类问题时的应用具有很高的价值。异常检测通常应用于金融欺诈检测、网络...
通过这个"关于Java异常的练习",你可以实践如何有效地处理异常,理解何时使用不同的关键字,以及如何编写符合最佳实践的异常处理代码。通过对`demo9`等示例代码的分析和修改,加深对Java异常处理机制的理解,提升...
异常通常通过异常类的对象来表示,这些对象包含了关于异常的详细信息。例如,数组下标越界、内存不足、磁盘访问错误或网络访问错误等都可以引发异常。当异常发生时,程序会创建一个异常对象并将其传递给Java运行时...
EXCEPTION_RECORD结构包含了关于异常的所有信息,包括异常代码、异常地址等;而CONTEXT结构则保存了处理器的状态信息,如寄存器的值等。在处理线程异常时,这两个结构通常会被用来获取和分析异常发生时的详细情况,...
Java中的异常处理是编程中非常重要的一个环节,它允许开发者优雅地处理程序中出现的错误,防止程序因未预期的问题而崩溃。异常是程序在运行时遇到的错误,比如除0溢出、数组越界、文件找不到等。Java提供了一整套...
在Java编程语言中,异常处理是一项至关重要的技能,尤其对于初学者来说,理解并熟练掌握异常处理机制是构建健壮程序的基础。Java异常处理的主要目的是中断正常的代码流程,处理程序运行时可能出现的问题,如文件未...
最后,关于异常的继承,Java规定如果子类方法覆盖了父类的方法,那么它可以声明抛出父类方法所声明的异常的子类,但不允许声明抛出比父类方法声明的异常更严格的异常。如果父类方法没有声明抛出任何异常,则子类方法...
代码用于测试c++在文件处理部分的异常操作,同时也对用户输入数据的时候就行了判断,在用户输入性别的时候进行了判断,由于时间问题就没有更多的进行判断。在这里使用到了类的相关功能,同时也做到了保证每次写入...
另一个是描述符,包含了关于异常的一些额外信息,如优先级、处理权限等。 4. **编程接口**:在编程中,我们需要使用特定的API或汇编指令来设置和修改异常向量表。例如,在x86系统中,可以使用`lidt`指令加载中断...
5. **异常链**:Java允许创建异常链,这样就可以保留关于异常起源的完整信息。一个异常可以引用导致它的另一个异常。 6. **多catch块**:Java 7引入了多catch语句,允许在一个catch块中处理多种类型的异常,提高了...
在编程世界中,异常处理是一种重要的错误处理机制,它允许程序在出现异常情况时优雅地进行恢复,而不是突然崩溃。C语言和C++都提供了异常处理功能,但它们的实现方式和理念有所不同。 C语言的异常处理主要是通过...
一些研究者,如Aggarwal等人,提供了广泛的关于异常检测技术的概述。这些技术包括但不限于基于统计的方法、机器学习方法、聚类方法、密度方法和基于邻近度的方法等。 综上所述,时序数据异常检测是一个跨学科的领域...
在Java编程中,异常处理是一项至关重要的技术,它确保了程序在遇到错误或异常情况时仍能保持稳定性和可靠性。异常是在程序编译或运行时出现的错误,这些错误可能会中断程序的正常流程。Java提供了丰富的异常处理机制...
异常确定系统是信息技术领域中的一个重要概念,主要用于检测和识别数据流、系统行为或网络通信中的不寻常模式。这种系统能够帮助我们发现潜在的问题,比如网络安全威胁、设备故障、欺诈行为或者业务过程中的异常情况...
自定义异常通常继承自`Exception`类或其子类,并且可以覆盖构造方法来传递更多关于异常的信息。 #### 六、异常处理示例 下面通过一个简单的示例来展示如何使用异常处理机制: ```java public static void main...
我们描述了复合希格斯模型的异常结构,其中最小模型的SO(5)/ SO(4)陪集结构由一个额外的,非线性实现的U(1)α扩展。 另外,我们表明有效的拉格朗日方程式允许一个术语,如QCD的手性拉格朗日方程中的Wess–“ ...
异常信号隐蔽性高,分析难度大,使得无数工程师都败倒在她的石榴裙下,但因其在信号的分析与调试过程中影响很大,工程师们不得不屡败屡战,一路坎坷前行。本文将结合实例进行分析,分享了一种新颖而实用的异常信号...