使用“goto”语句不好吗?
Posted
技术标签:
【中文标题】使用“goto”语句不好吗?【英文标题】:Is using a 'goto' statement bad? 【发布时间】:2012-08-08 00:21:33 【问题描述】:在研究了如何突破二级循环之后
while (true) // Main Loop
for (int I = 0; I < 15; I++) // Secondary loop
// Do Something
break; // Break main loop?
大多数人建议调用“goto”函数 如下例所示:
while (true) // Main Loop
for (int I = 0; I < 15; I++) // Secondary Loop
// Do Something
goto ContinueOn; // Breaks the main loop
ContinueOn:
但是;我经常听说“goto”语句是不好的做法。下图完美地说明了我的观点:
所以
goto 语句到底有多糟糕,为什么? 有没有比使用 'goto' 语句更有效的方法来中断主循环?【问题讨论】:
Is there a more effective way to break the main loop than using the 'goto' statement?
这不是一个合法的问题吗?
goto 看起来更干净。但是话又说回来,我无法回答,因为它会被否决。
这让我觉得这是一种摆脱深度嵌套循环的完全合理的方式。 GOTO 因其与行号一起使用而获得了“伟大的撒旦”的声誉,并且在需要在函数中包含某些内容的情况下不必要地使用。在 JS 中,我们标记了循环,可以轻松地在任何级别跳出嵌套循环。我认为这没有任何问题,尽管我会添加一个解释评论,以避免那些并不真正理解对 goto 的厌恶的开发人员下意识的歇斯底里。只要您没有在任何地方设置 GOTO,而是在循环或外部循环结束时,IMO 就没有气味。
If we're going to debate whether or not GOTO is still a bad practice in general, then this is a duplicate. 否则,将讨论保持在他的代码本地。
@FrançoisWahl 是的,我指的是整个嵌套循环结构是一个单元的情况,将真正独立的单元分离成它们自己的函数不会有任何问题。例如,在没有父循环的情况下对我的 Inner
函数进行单元测试是没有任何意义的。
【参考方案1】:
编辑:
goto 语句到底有多糟糕,为什么?
这取决于具体情况。我不记得我发现它使代码比重构更具可读性的任何时候。这还取决于您对可读性的个人看法 - 有些人比其他人更不喜欢它,从其他答案中可以清楚地看出。 (作为一个兴趣点,它广泛用于生成代码 - C# 5 中的所有 async/await 代码实际上都基于大量 goto。
问题在于,倾向于使用goto
的情况往往 是那种无论如何都需要重构的情况 - 而goto
坚持使用一种变得更难遵循的解决方案代码变得更复杂了。
有没有比使用'goto'语句更有效的方法来打破主循环?
当然。将您的方法提取到一个单独的函数中:
while (ProcessValues(...))
// Body left deliberately empty
...
private bool ProcessValues()
for (int i = 0; i < 15; i++)
// Do something
return false;
return true;
我通常更喜欢这样做而不是引入一个额外的局部变量来跟踪“我完成了吗”——当然这也可以。
【讨论】:
@MarceloAssis 你会递归什么?他的循环要么返回真/假,然后你继续基于此。我一点也不觉得它令人困惑。 我并不是说它不好,它确实是一个聪明的解决方案。但我花了很多时间才明白。 理想情况下,方法应该是可重用的。至少它们应该是逻辑上不同的工作单元。我认为这不符合这两个条件。这种解决方案使代码更难阅读和理解,仅仅是因为对goto
语句的非理性恐惧——这种恐惧是 1970 年代遗留下来的。
@MgSam:它绝对是一个独特的工作单元:它执行一个循环处理值,并返回是否还有更多工作要做。如果我们有更多的上下文,我当然可以给它一个更好的名字,但我们真的不知道这里发生了什么。至于可重用——我绝对没有提取私有方法,当它增强可读性时,它只能从另一种方法调用——我相信在这种情况下确实如此。
我实际上认为goto
是一种更直接的脱离主循环的方式,我认为使用它并没有真正的缺点【参考方案2】:
我强烈反对这里的所有其他答案。您使用goto
提供的代码没有任何问题。有一个原因C#有一个goto
语句,正是针对你描述的这些类型的场景。
goto
只是有一个负面的污名,因为在 1970 年代和之前的人们会编写可怕的、完全不可维护的代码,因为goto
,控制流会到处乱跳。 C# 的goto
甚至不允许在方法之间转换!然而,这种非理性的污名仍然存在。
在我看来,使用“现代”goto
打破内部循环绝对没有错。人们提供的“替代方案”最终总是更复杂,更难阅读。
方法通常应该是可重复使用的。为循环的内部部分创建一个完全独立的方法,该方法只会从那个位置调用,并且方法实现可能最终位于源代码中某个较远的位置,这并不是一种改进。
【讨论】:
该 wiki 链接中的内容在很大程度上不适用于 C#。正如我所提到的,您不能在 C# 中使用goto
的方法之间跳转。对于像 C 或 C++ 这样的语言,你可以用goto
做这些疯狂的事情,我同意这是危险的。在 C# 中,它存在主要是为了打破嵌套循环。【参考方案3】:
在这里添加其他答案,除了打破嵌套循环之外,goto 的一个巧妙用途是简单而干净的状态机:
goto EntryState;
EntryState:
//do stuff
if (condition) goto State1;
if (otherCondition) goto State2;
goto EntryState;
State1:
//do stuff
if (condition) goto State2;
goto State1;
State2:
//do stuff
if (condition) goto State1;
goto State2;
这是一种非常干净且易读的状态机方法,而且它的性能是免费的! 虽然您可以在没有 goto 的情况下执行此操作,但实际上只会降低您的代码的可读性。不要回避 goto,在使用前请三思。
【讨论】:
状态机的情况经常被忽视,它是直接goto
语句优于其他结构的一个很好的用例。【参考方案4】:
我有时使用“goto”,我发现它看起来不错,就像上面的例子;
bool AskRetry(Exception ex)
return MessageBox.Show(you know here...) == DialogResult.Retry;
void SomeFuncOrEventInGui()
re:try SomeThing();
catch (Exception ex)
if (AskRetry(ex)) goto re;
else Mange(ex); // or throw or log.., whatever...
我知道你可以递归地做同样的事情,但谁在乎它只是工作,我使用。
【讨论】:
同意并赞成。许多重试循环使用 goto 语句更容易阅读。正如其他人所说,使用一堆名为“成功”或类似名称的局部变量比使用 goto 重试操作要笨拙得多。 UI 驱动的重试逻辑往往会使结构良好的方法变得一团糟,goto 可以真正简化流程。也就是说,我们有一个庞大的代码库,使用它们的需求只出现了十几次。我的建议:如果其他结构不符合要求,请使用它们,但有疑问,它们是否真的简化了代码? 这绝对是一种更简洁的做事方式,然后不得不尝试将你的头脑围绕在一个递归函数上。代码应该易于理解,而且非常简单。【参考方案5】:我的一位同事(在固件编程方面有 15 年以上的经验),我一直在使用 goto。但是,我们只用它来处理异常!!例如:
if (setsockopt(sd,...)<0)
goto setsockopt_failed;
...
setsockopt_failed:
close(sd);
据我所知,这是在 C 中处理异常的最佳方式。我相信,我也为 C# 工作。另外,我不是唯一一个这么想的人:Examples of good gotos in C or C++
【讨论】:
【参考方案6】:goto 语句到底有多糟糕,为什么?
出于所有正常原因,这真的很糟糕。在不支持它们的语言中模拟带标签的循环时非常好。
在许多情况下,用函数替换它会分散真正应该作为同一个单元读取的逻辑。这使得它更难阅读。 没有人喜欢跟踪那些直到旅程结束时才真正做任何事情的函数,当你有点忘记的时候 你从哪里开始。
用布尔值和一堆额外的 if 和 break 代替它真的很笨拙,而且更难遵循真实意图,就像任何噪音一样。
在java(和javascript)中,这是完全可以接受的(标记循环):
outer: while( true )
for( int i = 0; i < 15; ++i )
break outer;
在 C# 中,看起来非常接近的等价物不是:
while( true )
for (int I = 0; I < 15; I++)
goto outer;
outer:;
因为goto
这个词,有一种心理作用,让人放下所有的常识,让他们不顾上下文地链接xkcd。
有没有比使用'goto'语句更有效的方法来打破主循环?
在某些情况下没有,这就是其他语言提供标记循环而 C# 提供goto
的原因。请注意,您的示例太简单了
变通方法看起来还不错,因为它们是针对示例量身定制的。事实上,我也可以这样建议:
for (int I = 0; I < 15; I++)
break;
这个怎么样:
int len = 256;
int val = 65536;
for (int i = 0; i < len; i++)
for (int j = 0; j < len; j++)
if (i + j >= 2 * val)
goto outer;
val = val / 2;
outer:;
你觉得这还不错吗:
int len = 256;
int val = 65536;
for (int i = 0; i < len; i++)
if (!Inner(i, ref val, len))
break;
private bool Inner(int i, ref int val, int len)
for (int j = 0; j < len; j++)
if (i + j >= 2 * val)
return false;
val = val / 2;
return true;
【讨论】:
+1 用于将goto
与return
和break
进行比较。与return
或break
的可能误用次数相比,查看goto
可能的误用和不必要的副作用,我明白为什么goto
不是最喜欢的。然而,这并不是因为goto
一定有问题,而是更多的错误使用它。我认为尽管您的帖子提出了一个非常有效的观点。【参考方案7】:
就我个人而言,我喜欢将 goto 视为“goto 会导致地狱”......在许多方面这是正确的,因为它可能导致高度不可维护的代码和非常糟糕的做法。
话虽如此,它仍然被实施是有原因的,并且在使用时,如果没有其他解决方案可用,则应谨慎使用。 OO 语言本身并不真正需要它(很多)。
这些天我看到它使用的唯一时间是混淆......一个使用 goto 从地狱创建代码的好例子,所以人们会被阻止尝试理解它!
某些语言依赖于等效的关键字。例如,在 x86 汇编中,您有诸如 JMP(跳转)、JE(如果相等则跳转)和 JZ(如果为零则跳转)等关键字。这些在汇编语言中经常需要,因为在此级别没有 OO,并且在应用程序中移动的其他方法很少。
AFAIK...除非绝对必要,否则请远离它。
【讨论】:
您能否详细说明为什么“goto 会导致地狱”?它被用于混淆并不意味着它本身正在混淆代码,Correlation != Causation。 相反,我可以使用goto
进行参数for,因为它提高了状态机的可读性,简化了try-catch-retry 逻辑的实现并减少了不必要的缩进。教条式的答案对任何人都没有帮助。以上是关于使用“goto”语句不好吗?的主要内容,如果未能解决你的问题,请参考以下文章