【问题标题】:Is using a labeled break a good practice in Java?在 Java 中使用标记中断是一种好习惯吗?
【发布时间】:2013-02-04 07:22:25
【问题描述】:

我在看一些 2001 年的旧代码,发现了这个声明:

   else {
     do {
       int c = XMLDocumentFragmentScannerImpl.this.scanContent();
       if (c == 60) {
         XMLDocumentFragmentScannerImpl.this.fEntityScanner.scanChar();
         XMLDocumentFragmentScannerImpl.this.setScannerState(1);
         break label913;
       }

我以前从未见过这个,并在这里发现了标记的中断:

http://docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html

这本质上不是像goto 那样工作吗?使用它甚至是一种好习惯吗?这让我很不安。

【问题讨论】:

  • label913 定义在哪里?
  • 在这种情况下,大约 200 行之后在一个相当大的 switch 语句的末尾。
  • 继续,标签名称将使代码处理代码块中标有标签的那部分代码
  • 在这种情况下,那么,问题在于switch语句的大小!
  • 这看起来像是生成的代码(实际上是一个解析器)。生成的代码中使用了很多不会在手写代码中使用的技巧(主要是因为它们使生成更容易)。

标签: java


【解决方案1】:

不,它不像 goto,因为您不能“转到”控制流的另一部分。

从您链接的页面:

break 语句终止带标签的语句;它不会将控制流转移到标签。控制流被转移到紧跟在标记(终止)语句之后的语句。

这意味着您只能中断当前正在执行的循环。

考虑这个例子:

first:
for( int i = 0; i < 10; i++) {
  second:
  for(int j = 0; j < 5; j ++ )
  {
    break xxx;
  }
}

third:
for( int a = 0; a < 10; a++) {

}

您可以将xxx 替换为firstsecond(以中断外部或内部循环),因为当您点击break 语句时正在执行两个循环,但将xxx 替换为@ 987654328@ 不会编译。

【讨论】:

  • docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html 如果将 continue 与标签结合起来将作为 goto 工作
  • @user_CC 并非如此,您仍然无法跳出控制流。带标签的 continue 只会开始带标签的循环的下一次迭代,并且只会在该循环已经执行时工作。
  • 所以如果我使用break first;,编译器会在first: 之后立即返回该行并再次执行这些循环吗?
  • @JohnnyCoder 不,break first;中断(结束)标记为first 的循环,即它将继续使用third。另一方面,continue first; 将结束 first 的当前迭代并同时中断 second 并将继续 first 中的 i 的下一个值。
  • @TheTechExpertGuy 好吧,不是真的。它只是类似于goto,在某种意义上或多或少与if-else 语句相同。 Goto 比“去任何地方”更“灵活”,例如到循环之前的位置(事实上,这就是旧语言用来实现循环的方式:“如果条件不满足,则转到循环体的开始(再次)”)。我最近不得不和他们打交道,相信我:goto 真的很痛苦,而标记的休息则不是。
【解决方案2】:

它不像goto 那样可怕,因为它只将控制发送到标记语句的末尾(通常是循环构造)。使goto 与众不同的是,它是任何地方的任意分支,包括在方法源代码中更高的标签,因此您可以拥有本地循环行为。 Java 中的标签中断功能不允许这种疯狂行为,因为控制只会向前推进。

大约 12 年前,我只使用过一次,当时我需要跳出嵌套循环,结构更结构化的替代方案可以在循环中进行更复杂的测试。我不建议经常使用它,但我不会将其标记为自动错误代码气味。

【讨论】:

  • @Boann:我并不是说 goto 在所有情况下都是邪恶的。它只是 Java 的创建者认为不值得麻烦的指针算术、运算符重载和显式内存管理之类的事情之一。并且阅读了似乎超过 90% 的 goto 的 COBOL 程序,我并不难过他们把它留在 Java 之外。
  • 哦,你第一次是对的@NathanHughes,goto 真是太邪恶了。当然不是在所有情况下,但是是的,我不会错过它。
  • goto 可能比标签更“有用”,@Boann,但这完全被他们的维护成本所抵消。标签不是 goto 的魔杖,这是一件好事。
【解决方案3】:

总是可以用新方法替换break

考虑代码检查两个列表中的任何公共元素:

List list1, list2;

boolean foundCommonElement = false;
for (Object el : list1) {
    if (list2.contains(el)) {
        foundCommonElement = true;
        break;
    }
}

你可以这样改写:

boolean haveCommonElement(List list1, List list2) {
    for (Object el : list1) {
        if (list2.contains(el)) {
            return true;
        }
    }
    return false;
}

当然,要检查两个列表之间的共同元素,最好使用list1.retainAll(new HashSet&lt;&gt;(list2)) 方法在O(n)O(n) 额外内存中执行此操作,或者在O(n * log n) 对两个列表进行排序,然后在O(n).

【讨论】:

  • 在我看来,这几乎总是正确的方法。 goto 是(潜在的)有害的,因为它可以将你送到很多地方——它正在完成什么并不明显;创建一个辅助函数为下一个人提供了明确的界限,让您知道您正在尝试做什么以及您将在哪里做,并且它让您有机会命名该功能。这样更清楚。
  • @ethanbustad 但这是关于标记为break,而不是goto。当然,这里显示的重构在这两种情况下都是一个好主意。但是标记为break 的代码不像goto 那样不受约束且容易出现意大利面条式的代码。
  • 这个例子很糟糕,break 不起作用 if,这里的标签只是没用。
  • 哇,我喜欢这个主意。它一直是痛苦的打破或继续外循环。谢谢。
【解决方案4】:

在阅读此答案的其余部分之前,请阅读Go To Statement Considered Harmful。如果你不想完整阅读,这里是我认为的重点:

随意使用 go to 语句的直接后果是,很难找到一组有意义的坐标来描述流程进度。

或者换种说法,goto 的问题在于程序可以到达代码块的中间,而程序员不了解此时的程序状态。标准的面向块的构造旨在清楚地描述状态转换,标记为break 旨在将程序带到特定的已知状态(包含标记块的外部)。

在现实世界的命令式程序中,状态并没有由块边界清楚地描述,因此标记break 是否是一个好主意值得怀疑。如果块改变了从块外部可见的状态,并且有多个点可以退出块,则标记为break 等效于原语goto。唯一的区别是,您不会有机会在状态不确定的块中间着陆,而是开始一个状态不确定的新块。

所以,一般来说,我会认为标记为break 是危险的。在我看来,这表明该块应该被转换为一个函数,对封闭范围的访问是有限的。

但是,这个示例代码显然是解析器生成器的产物(OP 评论说它是 Xerces 源代码)。解析器生成器(或一般的代码生成器)通常会随意使用它们生成的代码,因为它们对状态有完美的了解,而人类不需要理解它。

【讨论】:

  • 我不同意。 break 在很多场合都非常有用。例如,当您正在寻找可能来自多个有序来源的某个值时。你一个一个地尝试,当找到第一个时,你break。它是比if / else if / else if / else if 更优雅的代码。 (至少它的行数更少。)将所有内容移动到方法的上下文中可能并不总是可行的。另一种情况是,如果您有条件地从几个嵌套级别中断。简而言之,如果你不喜欢break,那么除了方法的结尾之外,你也不喜欢return
  • 用于上下文;当dowhile 之类的东西不存在并且每个人都在到处使用if(.. ) goto 时,编写了“Go To Statement Considered Harmful”。它旨在鼓励语言设计者添加对dowhile 之类的支持。可悲的是,一旦这股潮流开始滚动,它就变成了一列炒作,由无知的人推动,将break 之类的东西放入“只执行一次”循环中,并抛出异常给谁知道,对于goto 的所有(罕见)案例更清洁,危害更小。
【解决方案5】:

至少在我看来,表达意图的一种更简洁的方式可能是将包含循环的代码片段放入一个单独的方法中,并从中简单地return

例如,改变这个:

someLabel:
for (int j = 0; j < 5; j++)
{
    // do something
    if ...
        break someLabel;
}

进入这个:

private void Foo() {
    for (int j = 0; j < 5; j++)
    {
        // do something
        if ...
            return;
    }
}

这对于精通其他语言的开发人员来说也更惯用,将来可能会使用您的代码(或future you)。

【讨论】:

    【解决方案6】:

    这不像gotostatement 中你向后跳转流控制。标签仅向您(程序员)显示中断发生的位置。另外,流控制转移到break之后的下一条语句。

    关于使用它,我个人认为它没有什么大用处,因为一旦你开始编写价值数千行的代码,它就变得微不足道了。但同样,这取决于用例。

    【讨论】:

    • for(int i = 0; i &lt; 1; i++) { do_something(); if(I_want_to_go_backwards) { i = 0; continue; } }.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-26
    • 2012-08-14
    • 2018-08-21
    • 2015-06-23
    • 1970-01-01
    • 2016-02-10
    相关资源
    最近更新 更多