Eli*_*sky 88 c error-handling exception-handling goto
这个问题实际上是不久前在programming.reddit.com 上进行有趣讨论的结果.它基本归结为以下代码:
int foo(int bar)
{
int return_value = 0;
if (!do_something( bar )) {
goto error_1;
}
if (!init_stuff( bar )) {
goto error_2;
}
if (!prepare_stuff( bar )) {
goto error_3;
}
return_value = do_the_thing( bar );
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
Run Code Online (Sandbox Code Playgroud)
goto这里的使用似乎是最好的方法,导致所有可能性中最干净,最有效的代码,或者至少在我看来.在Code Complete中引用Steve McConnell :
goto在分配资源,对这些资源执行操作,然后释放资源的例程中很有用.使用goto,您可以清理代码的一部分.goto可以降低忘记在检测到错误的每个位置释放资源的可能性.
此方法的另一个支持来自本节中的" Linux设备驱动程序"一书.
你怎么看?这种情况goto在C中是否有效?您是否更喜欢其他方法,这些方法会产生更复杂和/或效率更低的代码,但是要避免goto?
Mic*_*urr 55
FWIF,我发现你在问题的例子中给出的错误处理习惯比目前为止答案中给出的任何替代方案更具可读性和易于理解.虽然goto通常是一个坏主意,但是当以简单和统一的方式完成时,它对于错误处理是有用的.在这种情况下,即使它是a goto,它也是以明确定义的,或多或少结构化的方式使用的.
Jon*_*ler 16
作为一般规则,避免使用goto是一个好主意,但是当Dijkstra第一次写"GOTO Considered Harmful"时流行的滥用行为现在甚至没有超过大多数人的想法.
您概述的是错误处理问题的一般解决方案 - 只要仔细使用它就可以了.
您的特定示例可以简化如下(步骤1):
int foo(int bar)
{
int return_value = 0;
if (!do_something(bar)) {
goto error_1;
}
if (!init_stuff(bar)) {
goto error_2;
}
if (prepare_stuff(bar))
{
return_value = do_the_thing(bar);
cleanup_3();
}
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
Run Code Online (Sandbox Code Playgroud)
继续这个过程:
int foo(int bar)
{
int return_value = 0;
if (do_something(bar))
{
if (init_stuff(bar))
{
if (prepare_stuff(bar))
{
return_value = do_the_thing(bar);
cleanup_3();
}
cleanup_2();
}
cleanup_1();
}
return return_value;
}
Run Code Online (Sandbox Code Playgroud)
我认为,这相当于原始代码.这看起来特别干净,因为原始代码本身非常干净且组织良好.通常,代码片段并不像那样整洁(虽然我接受他们应该的论点); 例如,经常有更多的状态传递给初始化(设置)例程而不是显示,因此更多的状态也传递给清理例程.
psm*_*ars 15
我很惊讶没有人提出这个替代方案,所以尽管这个问题已经存在了一段时间我会加入它:解决这个问题的一个好方法是使用变量来跟踪当前状态.这是一种可以使用的技术,无论是否goto用于到达清理代码.像任何编码技术一样,它有优点和缺点,并不适合所有情况,但如果你选择一种风格,值得考虑 - 特别是如果你想避免goto不用深度嵌套的ifs.
基本思想是,对于可能需要采取的每个清理操作,都有一个变量,从中可以判断清理是否需要进行清理.
我将goto首先显示该版本,因为它更接近原始问题中的代码.
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
/*
* Prepare
*/
if (do_something(bar)) {
something_done = 1;
} else {
goto cleanup;
}
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
goto cleanup;
}
if (prepare_stuff(bar)) {
stufF_prepared = 1;
} else {
goto cleanup;
}
/*
* Do the thing
*/
return_value = do_the_thing(bar);
/*
* Clean up
*/
cleanup:
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Run Code Online (Sandbox Code Playgroud)
与其他一些技术相比,这样做的一个优点是,如果初始化函数的顺序发生变化,仍然会发生正确的清理 - 例如,使用switch另一个答案中描述的方法,如果初始化顺序发生变化,那么switch必须非常仔细地编辑,以避免尝试清理一些事实上没有初始化的东西.
现在,有些人可能认为这种方法增加了许多额外的变量 - 事实上在这种情况下确实如此 - 但实际上现有的变量通常已经跟踪或者可以跟踪所需的状态.例如,如果prepare_stuff()实际上是对malloc()或的调用open(),则可以使用保存返回的指针或文件描述符的变量 - 例如:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Run Code Online (Sandbox Code Playgroud)
现在,如果我们另外使用变量跟踪错误状态,我们可以goto完全避免,并且仍然可以正确清理,而不会让缩进越来越深,我们需要的初始化越多:
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
int oksofar = 1;
/*
* Prepare
*/
if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */
if (do_something(bar)) {
something_done = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (prepare_stuff(bar)) {
stuff_prepared = 1;
} else {
oksofar = 0;
}
}
/*
* Do the thing
*/
if (oksofar) {
return_value = do_the_thing(bar);
}
/*
* Clean up
*/
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Run Code Online (Sandbox Code Playgroud)
同样,有可能批评这一点:
if (oksofar)检查的顺序优化为单个跳转到清理代码(GCC肯定会) - 并且在任何情况下,错误情况通常对性能不太重要.这不是另外一个变量吗?在这种情况下是,但通常该return_value变量可用于oksofar扮演在这里播放的角色.如果您构建函数以一致的方式返回错误,您甚至可以避免if在每种情况下的第二个:
int return_value = 0;
if (!return_value) {
return_value = do_something(bar);
}
if (!return_value) {
return_value = init_stuff(bar);
}
if (!return_value) {
return_value = prepare_stuff(bar);
}
Run Code Online (Sandbox Code Playgroud)
这样编码的一个优点是,一致性意味着原始程序员忘记检查返回值的任何地方都像拇指一样伸出,这样就更容易找到(那一类)错误.
所以 - 这是(还)一种可以用来解决这个问题的风格.正确使用它可以提供非常干净,一致的代码 - 就像任何技术一样,在错误的手中它最终会产生冗长且令人困惑的代码:-)
goto关键字的问题大多被误解了.这不是简单的邪恶.您只需要了解每个goto创建的额外控制路径.很难推断出你的代码及其有效性.
FWIW,如果你查看developer.apple.com教程,他们会采用goto方法来处理错误.
我们不使用gotos.对返回值的重要性更高.异常处理是通过setjmp/longjmp- 无论你能做什么.
goto 语句在道德上没有任何错误,就像 (void)* 指针在道德上没有错误一样。
一切都取决于您如何使用该工具。在您提出的(简单的)案例中,案例语句可以实现相同的逻辑,尽管开销更大。真正的问题是“我的速度要求是多少?”
goto 的速度非常快,尤其是当您小心确保它编译为短跳转时。非常适合注重速度的应用。对于其他应用程序,为了可维护性,使用 if/else + case 来承受开销可能是有意义的。
请记住:goto 不会杀死应用程序,而是开发人员杀死应用程序。
更新:这是案例示例
int foo(int bar) {
int return_value = 0 ;
int failure_value = 0 ;
if (!do_something(bar)) {
failure_value = 1;
} else if (!init_stuff(bar)) {
failure_value = 2;
} else if (prepare_stuff(bar)) {
return_value = do_the_thing(bar);
cleanup_3();
}
switch (failure_value) {
case 2: cleanup_2();
case 1: cleanup_1();
default: break ;
}
}
Run Code Online (Sandbox Code Playgroud)