|
本文想聊一聊软件的异常处理,这里的异常不是指那种可捕获的异常机制(捕捉和抛异常),而是指程序运行时可能出现的错误。 简单的说,就是软件运行的时候出现错误应该如何处理比较好。
对程序运行时可能出现的错误是否恰当的处理,是衡量代码质量的一个重要的指标,也是衡量一个程序员是否合格的一个重要指标。
目前编程语言对软件运行错误的处理方式有两种:
- 返回错误值 返回一个错误值,可以是一个简单的数字(错误码)或者其他类型的值,比如C、Go。
- 抛异常 比如C++、Java、JavaScript、Python等。
对于 C 和 Go 还好,反正只支持一种异常处理方式,代码只能那样写,但对于其他编程语言来说,由于两种方式都支持,具体应该用那种方式,就需要仔细考虑了,实际工作中,我发现很多人经常弄不清楚,两种方式经常误用滥用。
返回错误值
返回错误是早期编程语言处理程序异常的唯一方式,这种方式其实是一种很自然的方式,简单有效,易于理解。 早期语言最有代表性的就是 C 语言了,下面就以 C 语言为例,来看看具体代码。
Linux 的打开文件的 API 如下:
int open(const char *pathname, int flags);
系统自带的 man 文档,对其返回值是这样描述的:
open() return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).
意思就是调用成功时返回文件描述符,失败则返回 -1,同时全局变量 errno 里保存错误码。所以一般错误处理就是下面这样。
int fd1, fd2;
fd1 = open("test1.txt", 0); // 打开test1.txt文件
if (fd1 == -1) {
// 打开失败,打印原因, strerror用于把错误码转为文字描述
printf("open fail, errno: %d, %s\n", errno, strerror(errno));
return -1;
}
fd2 = open("test2.txt", O_CREAT); // 打开test2.txt文件
if (fd2 == -1) {
printf("open fail, errno: %d, %s\n", errno, strerror(errno));
return -1;
}
char buf[512];
ssize_t n;
n = read(fd1, buf, sizeof(buf)); // 从test1.txt读取数据
if (n == -1) {
printf("read fail, errno: %d, %s\n", errno, strerror(errno));
return -1;
}
n = write(fd2, buf, n); // 写入test2.txt
if (n == -1) {
printf("write fail, errno: %d, %s\n", errno, strerror(errno));
return -1;
}
这种处理方式是非常直观的,所有处理逻辑都是由程序员来写的,语言本身并不需要提供什么看不见的底层处理(所以很快),但同时这种处理方式对每个可能出错的地方都需要编写一堆处理代码,非常繁琐,容易造成代码里到处都是异常处理代码,把主逻辑都给淹没了。
实际开发过的 C 项目中,采用过一种宏定义方式,来降低这种繁琐。
err.h
#define CALL(func)\
do {\
int __ec = (func);\
if (__ec < 0) {\
printf(&#34;%s return %d, %s\n&#34;, #func, __ec, strerror(__ec));\
return __ec;\
}\
} while (0)
app.c
#include &#34;err.h&#34;
int fd1, fd2;
CALL(fd1 = open(&#34;test1.txt&#34;, 0)); // 打开test1.txt文件
CALL(fd2 = open(&#34;test2.txt&#34;, 0)); // 打开test2.txt文件
char buf[512];
ssize_t n;
CALL(n = read(fd1, buf, sizeof(buf)));
CALL(write(fd2, buf, n));
这种方式对每个需要处理异常的函数,调用时需要用宏 CALL() 包起来,虽然还是有点丑陋,但还是比原始方式要方便许多。
一般来说。对 C 语言函数,有以下要求,
- 函数必须要描述清楚其可能返回的的所有错误值。
- 函数调用者必须正确处理所有错误值,不能忽略。
Go 语言的异常处理机制比 C 语言有所加强,但本质还是返回错误值形式。
抛异常
由于返回错误值机制具有以上所描述的一些弊端,后来的语言发展出了一种异常处理机制,该机制主要有以下内容。
- 提供一种异常类型,程序出错时可以抛出异常对象。
- 异常对象一旦被抛出,如果没有被捕获,则会一直向上传递。
- 程序可以捕获异常,以停止异常的向上传递,并处理异常。
来看看实际代码,以 Python 为例。
try:
f1 = open(&#39;test1.txt&#39;)
f2 = open(&#39;test2.txt&#39;)
buf = f1.read()
f2.write(buf)
except OSError as e:
print(e)可以看出,使用异常,可以避免像返回错误值那样,需要到处写异常处理代码的情况,从而可以让程序员更专注于业务逻辑。 异常机制的实现本质是语言底层做了一些看不见的处理,所以性能上会比返回错误值要差一些,当然一般不会差太多。
使用异常时,有一些使用原则,这也是很多新手容易犯的错误。
- 不要每小段语句都去捕获异常 应该尽可能在大的范围捕获异常,当然具体是多大范围,是要看具体业务逻辑的,一般只要逻辑允许,尽量扩大范围。这样可以避免太多异常捕获代码。让代码更简洁高效,否则就跟返回错误值机制一样了。
- 捕获具体的异常,不要捕获所有异常 捕获能处理的具体异常,而不是用通用异常类型,把所有异常都捕获。对于每个捕获的具体异常,做精确的处理,对于不能处理的异常,不要捕获让上层去捕获。
python try: call_func() except Exception as e: # 这种捕获所有异常的处理方式不好 print(e)
- 不要用抛异常来代替函数的正常返回值 函数抛出的异常一定要是表示某种错误,而不能是正常的返回值。
- 时刻注意异常会打断执行流 异常处理会像中断一样打断程序的正常执行流,这有可能会导致一些逻辑问题,需要关注并恰当处理 python lock() call_func() unlock() # 如果上面语句抛出异常,则这句不会被执行,造成死锁
正确的方式: python lock() try: call_func() finally: unlock()
- 函数要描述清楚其所有可能抛出的异常 应该包括直接和间接抛出的异常,即函数自己抛出的异常外,还应包括函数内部调用其他函数可能抛出并且不捕获的异常。
- 在程序的最外层捕获所有异常 虽然上面提到不要捕获所有异常,但是有个例外,一般在程序的最外层需要捕获所有异常,不然会导致程序退出。
什么时候返回错误值,什么时候抛异常
支持抛异常机制的编程语言也同时支持返回错误值机制,虽然抛异常机制可以让代码更简洁,但并不是所有的错误都应该使用抛异常来处理。 那到底什么时候返回错误值,什么时候抛异常?, 其实很简单,就一条原则。
对于程序实际运行时不应该发生或大概率不会发生的错误,才抛异常,否则返回错误值。
但是如果有个功能可能出现某种错误,该错误在一些使用场景下不应该发生,而在另一个使用场景下可以发生,这样的错误该怎么处理呢? 答案是针对不同场景,对外提供两种访问接口,分别用两种方式来处理.
比如 Python 的字典,就提供两种访问方式。
d = {&#39;a&#39;: 1, &#39;b&#39;: 2} # 字典,含有 key=&#39;a&#39; 和 key=&#39;b&#39; 两个数据
# 获取 key=&#39;c&#39; 的数据
value = d[&#39;c&#39;] # 抛异常 KeyError
value = d.get(&#39;c&#39;) # 返回 None当调用者明确知道字典里肯定有key的数据时,使用 d[] 方式,否则用 d.get(),
当然任何事情都不是绝对的,总有例外,因此有些时候确实会存在不好判断该用那种异常处理方式的情况,这时就看个人喜好了,觉得怎么简单怎么来。 |
|