昨晚翻了翻《松本行弘的程序世界》这本书,看到他对异常设计原则的讲述,觉得颇为赞同。近期的面试,我有时也问类似的问题,但应聘者的回答大都不能令人满意。有必要理一理,说说我是怎么理解的,以及在编程实践中如何做出合适的选择。当然这只是一家之言,未必就是完全正确的。
在行文之前,我有一个观点需要明确:错误码和异常,这两者在程序的表达能力上是等价的,它们都可以向调用者传达“与常规情况不一样的状态”。因此,要使用哪一种,是需要从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.