为什么我们应该尽量减少在循环中使用break和continue?

7

当我还是一年级新生时,我们的导师允许我们在循环中使用 breakcontinue。那时候我大多数情况下都这样做,因为它可以终止/继续循环。现在我已经是大二学生了,我的导师告诉我不建议使用 break/continue。你能告诉我原因吗?顺便问一下,break/continue 会造成什么影响呢?


19
他不能告诉你原因吗?那他在那里干什么? - trojanfoe
26
起初他们禁用了 goto,我没有发表任何言论,因为我并没有使用 goto。接着他们又禁用了 continuebreak,我也没有发表任何言论,因为那时我正在避免使用循环语句。最后他们禁用了我所需的代码语句... - Frédéric Hamidi
政治正确疯了。 - trojanfoe
你经常可以将一个 continue 转换为一个 if 语句。 - Neil Kirk
作为一个写过百万行C++代码的人,我仍然在循环中使用break和continue。然而,大部分情况下我并不这样做。如果我要猜的话,我会说大概只有100个循环中有1个会用到。 - drescherjm
3个回答

19

有些人认为,控制流太复杂是不好的,这意味着像breakcontinue和多个return之类的东西。原因不是技术上的,而是复杂的控制流可能会使程序更难以验证、测试和推理。

然而,这在很大程度上取决于个人风格、个人口味和整体结构。对于小型、目的明确的函数,可能没有什么问题可以有多个可能的流程。特别是在C++中,早期退出是一种常用的习惯,通常可以使代码更易于跟踪。


4
特别是在C++中,提前退出是一种流行的惯用语,经常可以使代码更易于理解。即使只有一个return语句,C++程序也可能具有非常复杂的控制流:在此之后可能会运行许多析构函数,更不用说异常了。但是,像所有东西一样,要适度使用。 - ta.speot.is
@ta.speot.is:嗯,是的,这就是重点:C++ 支持语言中的早期退出。我认为OP的建议大多适用于C。例如,我认为MISRA C规则强制执行单个返回。 - Kerrek SB
2
@KerrekSB 是的,规则14.7,函数中应该只定义一个出口点 - xanatos
@ta.speot.is 我不知道你从哪里得到这个观点的。我已经编写C++程序超过25年了,从未在循环中使用过breakcontinue(更不用说return了)。正如Kerrek所说,这会使得对代码正确性的推理变得更加困难。 - James Kanze
@JamesKanze,我正在处理一个遵循这个风格指南的代码库,我发现三层if语句和对result变量的赋值比仅使用return更加混乱。 - Bolpat

5
我看到的大部分关于代码正确性推理的逻辑都假定单一入口/单一出口。如果你的循环中充斥着break和continue,那就不可能知道你的循环不变式是否满足,或者你是否总是在前进(因此循环不会无限)。 (请注意,do {...} while (...); 循环也会受到这种情况的影响;第一次通过时不会建立循环不变式,这可能会导致一些意外情况。)
在大多数需要使用break或continue的情况下,您可能已经使循环(以及包含它的函数)过于庞大和复杂。
有人会争辩说:
for (;;) {
    //  ...
    if ( conditionMet ) {
        break;
    }
    //  ...
}

对于经典的循环半个习语来说,这是可以接受的;毕竟,它是单入口/单出口的,即使出口不在我们期望的位置上(并且在阅读代码时很难找到)。但关于循环不变量的问题仍然存在;它们在if之前没有满足,至少在第一次执行时如此。通常,更好的解决方案是将测试代码放入一个单独的函数中,该函数返回conditionMet,并使用:

while ( doLoopPrefix() ) {
    doLoopSuffix();
}

一般来说,如果您的循环超过三到四行,您应该进行重构。除非它包含单个switch语句,并且拥有很多情况。

5
至少在C语言中,你不应该像你过去做的那样大部分时间使用break和/或continue来控制循环的流程。你的循环条件应该表明在什么情况下循环应该停止;维护你的代码的人不应该必须深入到循环体中的代码中去查看触发break导致循环停止的原因是什么。
例如,假设你想从一个名为inputFile的文件中读取若干个整数并检查其中是���有一个整数是500。一种规范循环结构的方式如下:
while (fgets (buffer, sizeof (buffer), inputFile)){
    sscanf (buffer, "%d", &num);
    if (num == 500)
       break;
}

在这里,阅读代码的人必须读完整个while循环才能弄清楚您实际上要在文件中查找什么。如果您没有使用break编写此代码:

while ((num != 500) && fgets (buffer, sizeof (buffer), inputFile)) 
    sscanf (buffer, "%d", &num);

循环条件本身告诉读者代码试图做什么,这使得快速理解更加容易。而且,额外的好处是您已经节省了几行代码。
现在想象一下更复杂的while或for循环,其中break深埋在循环体内部。很容易看出为什么尝试查找break触发器会变得很烦人。拥有正确结构的循环条件更具自说明性。
当然,存在使用break和continue编写代码的情况。例如,如果循环结束的条件可能发生在循环执行的中间,并且存在一长串跟随在循环内部的语句,执行这些语句会增加处理时间而无益于完成任何有用的事情,那么可以使用break。但这些情况是例外,而不是“大多数”的情况。

能否请下投票者解释一下? - verbose
2
我不是那个给你点踩的人,但我认为你的观点有些无意义:你用一个恶劣的方法(break)来换取另一个复杂的循环条件。对我来说,第一个版本更清晰明了,我认为这是使用基本循环条件和其他使进一步处理变得多余的异常条件的方式。更重要的是,因为语法高亮会立即突出显示任何breakcontinue语句。无论如何,我认为你应该避免编写在其中无法立即发现break的循环。 - cmaster - reinstate monica
@cmaster 谢谢。我同意你关于“异常情况”需要break的观点,但在这个人为的例子中,搜索的结果是num == 500。这也是文件被读取的原因。用简单的英语表达,条件应该是:“查看文件中是否有数字500。”我认为循环条件更清晰地表示了这种逻辑。而且我不认为它过于复杂——当然了。基本观点是,代码的逻辑应该易于理解,而break可能会使其变得模糊,特别是如果“大多数时间”使用它。 - verbose
嗯,我同意你关于“大多数情况下”的观点 :-) 我只是没能看出 break 本身有什么邪恶之处... - cmaster - reinstate monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接