最近,Python Brasil 邮件列表上开始了关于使用异常的原因的讨论。有一次,一位非常有能力的参与者评论了通过函数返回来处理错误是多么困难,就像在 C 中一样。
当你有一个复杂的算法时,每个可能失败的操作都需要一系列的 if 来检查操作是否成功。如果操作失败,您需要恢复之前的所有操作以退出算法,而不改变程序的状态。
让我们看一个例子。假设我有以下结构来表示数组:
<a style="color:#f60; text-decoration:underline;" href="https://www.php.cn/zt/58423.html" target="_blank">typedef</a> 结构体 {
整数大小;
整数*数组;
} array_t;
现在,我将编写一个函数,从文本文件中读取要放置在这些数组之一中的元素数量,然后读取元素本身。该函数还将分配数组结构和数组本身。问题是这个函数很容易出错,因为我们可能会失败:
- 打开给定的文件;
- 分配结构体;
- 由于输入/输出错误或文件结尾而读取给定文件中的元素数量;
- 分配内存来存储要读取的元素;
- 由于输入/输出错误或文件结尾而读取其中一个元素。
很复杂吧?请注意,如果我们设法打开文件但无法分配结构,则必须关闭该文件;如果我们设法打开文件并分配结构体,但无法从文件中读取元素数量,则必须释放结构体并关闭文件;等等。因此,如果我们检查所有错误并采用在出现错误时返回 NULL 的传统,我们的函数将如下所示:
array_t *readarray(const char *文件名) {
文件*文件;
array_t *数组;
整数我;
文件 = fopen(文件名, "r");
如果(文件== NULL)返回NULL;
数组= malloc(sizeof(array_t));
如果(数组== NULL){
fclose(文件);
返回空值;
}
if (fscanf(文件, "%d", &(数组->大小)) == EOF) {
自由(数组);
fclose(文件);
返回空值;
}
数组->数组 = malloc(sizeof(int) * 数组->大小);
if (数组->数组 == NULL) {
自由(数组);
fclose(文件);
返回空值;
}
for (i = 0; i 大小; i++) {
if (fscanf(文件, "%d", 数组->数组 + i) == EOF) {
自由(数组->数组);
自由(数组);
fclose(文件);
返回空值;
}
}
返回数组;
}
确实,相当费力,而且有很多重复的代码……
但是请注意,上面的代码中有两种情况
- 其中,当我有两个操作要恢复时,我需要先恢复最后执行的一个,然后再恢复前一个。例如,当释放结构体和整数数组时,我需要先释放整数数组,然后释放结构体。如果我先释放结构体,以后可能无法释放数组。
- 在其他情况下,顺序并不重要。例如,如果我要释放结构体并关闭文件,那么执行此操作的顺序并不重要。这意味着我也可以先恢复最后执行的操作,然后恢复第一个操作。
这有什么意义?嗯,在实践中,我从未见过必须先恢复第一个执行的操作,然后恢复第二个,依此类推的情况。这意味着,当执行操作 a()、b()、c() 等时,恢复它们的“自然”方法是以相反的顺序调用恢复函数,如下所示:
a();
b();
C();
/* ... */
revert_c();
恢复b();
恢复a();
现在窍门来了。在上面的代码中,在每个操作之后,我们都会放置一个 if 来检查它是否失败。如果失败,将执行 goto 到上次成功操作的恢复函数:
a();
if (failed_a()) 转到 FAILED_A;
b();
if (failed_b()) 转到 FAILED_B;
C();
if (failed_c()) 转到 FAILED_C;
/* ... */
revert_c();
失败_C:
恢复b();
失败_B:
恢复a();
失败_A:
返回;
如果a()失败,算法返回;如果 b() 失败,算法将转到 FAILED_B:,恢复 a() 并返回;如果 c() 失败,算法将转到 FAILED_C,恢复 b(),恢复 a(),然后返回。你能看到图案吗?
如果我们将此模式应用于 readarray() 函数,结果将类似于:
array_t *readarray(const char *文件名) {
文件*文件;
array_t *数组;
整数我;
文件 = fopen(文件名, "r");
如果(文件== NULL)转到FILE_ERROR;
数组= malloc(sizeof(array_t));
如果(数组== NULL)转到ARRAY_ALLOC_ERROR;
if (fscanf(文件, "%d", &(数组->大小)) == EOF)
转到SIZE_READ_ERROR;
数组->数组 = malloc(sizeof(int) * 数组->大小);
if (array->array == NULL) 转到 ARRAY_ARRAY_ALLOC_ERROR;
for (i = 0; i 大小; i++) {
if (fscanf(文件, "%d", 数组->数组 + i) == EOF)
转到 ARRAY_CONTENT_READ_ERROR;
}
返回数组;
ARRAY_CONTENT_READ_ERROR:
自由(数组->数组);
ARRAY_ARRAY_ALLOC_ERROR:
大小读取错误:
自由(数组);
ARRAY_ALLOC_ERROR:
fclose(文件);
文件错误:
返回空值;
}
这种模式有什么优点?嗯,它减少了操作反转代码的重复,并将错误处理代码与功能逻辑分离。事实上,虽然我认为异常是最好的现代错误处理方法,但对于本地错误处理(在函数本身内部),我发现这种方法更实用。
(这篇文章是 Tratamento de error em C com goto 的翻译,最初发表于 Suspensão de Descrença。)