使用“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 用于将gotoreturnbreak 进行比较。与returnbreak 的可能误用次数相比,查看goto 可能的误用和不必要的副作用,我明白为什么goto 不是最喜欢的。然而,这并不是因为goto 一定有问题,而是更多的错误使用它。我认为尽管您的帖子提出了一个非常有效的观点。【参考方案7】:

就我个人而言,我喜欢将 goto 视为“goto 会导致地狱”......在许多方面这是正确的,因为它可能导致高度不可维护的代码和非常糟糕的做法。

话虽如此,它仍然被实施是有原因的,并且在使用时,如果没有其他解决方案可用,则应谨慎使用。 OO 语言本身并不真正需要它(很多)。

这些天我看到它使用的唯一时间是混淆......一个使用 goto 从地狱创建代码的好例子,所以人们会被阻止尝试理解它!

某些语言依赖于等效的关键字。例如,在 x86 汇编中,您有诸如 JMP(跳转)、JE(如果相等则跳转)和 JZ(如果为零则跳转)等关键字。这些在汇编语言中经常需要,因为在此级别没有 OO,并且在应用程序中移动的其他方法很少。

AFAIK...除非绝对必要,否则请远离它。

【讨论】:

您能否详细说明为什么“goto 会导致地狱”?它被用于混淆并不意味着它本身正在混淆代码,Correlation != Causation。 相反,我可以使用goto 进行参数for,因为它提高了状态机的可读性,简化了try-catch-retry 逻辑的实现并减少了不必要的缩进。教条式的答案对任何人都没有帮助。

以上是关于使用“goto”语句不好吗?的主要内容,如果未能解决你的问题,请参考以下文章

GOTO仍被视为有害吗? [关闭]

使用没有花括号的 if 语句是一种不好的做法吗? [关闭]

c#_goto语句

c#_goto语句

goto语句为啥不受待见

如果你大学上过编程课,一定被老师提醒过:不要使用 goto 语句!