“while (true) + break” 是否是不良的编程实践?

81

我经常使用这个代码模式:

while(true) {

    //do something

    if(<some condition>) {
        break;
    }

}   

另一位程序员告诉我这样做是不好的实践,建议我替换成更标准的:

while(!<some condition>) {

    //do something

}   
他的理由是你很容易“忘记break”并导致无限循环。我告诉他在第二个例子中,你同样可以添加一个条件永远不会返回true,因此也同样容易导致无限循环,所以两种方法都是同样有效的。
此外,当你有多个跳出循环的条件时,我通常更喜欢前一种方法,因为它使代码更易于阅读。
有谁能通过为其中一方添加证据来丰富这个论点?

2
如果我指出这个问题 http://stackoverflow.com/questions/212531/is-while-truebreakend-while-a-good-design 基本上是在问同样的事情,那么我会被视为一个脾气暴躁的人吗? - Jonathan Leffler
另一个类似的问题,有趣的答案:https://dev59.com/X3VC5IYBdhLWcg3wlyQo - Jim Nelson
23个回答

63

这两个示例之间存在差异。第一个示例将会在每次循环中至少执行一次“做某事”,即使语句从未为真。第二个示例仅在语句求值为true时才会执行“做某事”。

我认为你正在寻找的是do-while循环。我100%同意while(true)不是一个好主意,因为它使得代码难以维护,而且你逃离循环的方式非常类似于goto,这被认为是一种不良实践。

尝试:

do {
  //do something
} while (!something);

查看您所使用编程语言的文档,了解确切的语法。但是请看这段代码,它基本上执行 "do" 中的操作,然后检查 "while" 部分以确定是否需要再次执行。


7
在服务中,我经常使用 while(true) 来作为工作线程的循环体,只有在出现线程异常时才会返回。 - Tracker1
15
这个回答没有考虑到更常见的习语:while (true) { /* 做某事 / 如果(条件)break; / 做更多的事情 / } 这避免了重复执行“做某事”的部分:/ 做某事 / while (条件) { / 做更多的事情 / / 做某事 */ } - jmucchiello
在 while 循环语句中,如果我写 True 而不是 !something,这样做还可以吗?@Andrew G. Johnson - alper

35

引用那位著名的开发者Wordsworth的话:

...
实际上,我们所判定的牢狱并不是牢狱;因此对于我来说,在各种心境中,被束缚在十四行诗的狭窄领域之内,对我来说是一种消闲乐趣。如果有些灵魂(因为他们的需要必须如此)感受到了过多自由的重压,则可以在那里找到简短的慰藉,就像我一样。

Wordsworth接受十四行诗的严格要求作为解放性的框架,而不是紧身衣。我认为,“结构化编程”的核心是放弃在流程图中任意建立复杂流程的自由,以换取更加容易理解的解放感。

我完全同意,有时候早期退出是表达动作最简单的方式。然而,我的经验是,当我强制自己使用最简单的控制结构(并真正思考在这些限制内进行设计)时,我通常会发现结果是更简单、更清晰的代码。唯一的缺点是

while (true) {
    action0;
    if (test0) break;
    action1;
}

问题在于很容易让action0action1变成越来越大的代码块,或者添加“只是再一个”测试-断言-行动序列,直到难以指出具体的一行并回答问题,“我知道这一点条件成立吗?” 因此,在不为其他程序员制定规则的情况下,我尽可能避免在自己的代码中使用while (true) {...}习惯用法。


1
我不同意特定的观点(我认为while(true){}比while(condition){}更“纯粹”); 但我喜欢“严格要求作为解放名声”的想法。 - Javier
2
@joel:太棒了,你的答案!@Javier:goto语句本质上是最纯粹的,但一般来说,选择更有限制性和描述性的语句会更好。 - Shog9

26

当您可以以以下形式编写代码时

while (condition) { ... }
或者
while (!condition) { ... }

没有在循环体中使用退出语句(如breakcontinuegoto)时,我们更推荐使用这种形式的循环。 这是因为在查看头部时,其他人可以通过阅读代码并理解终止条件。这是很好的。

但是许多循环不符合这种模式,而在中间使用明确退出的无限循环是一种可敬的模式。(带有continue的循环通常比带有break的循环更难理解。)如果您需要证据或权威引用,请参阅Don Knuth关于带有Goto语句的结构化编程的著名论文;您会找到所有您想要的示例、论点和解释。

一个小习惯用法:写成while (true) { ... }将使您被视为老式Pascal程序员或者现在的Java程序员。如果您正在使用C或C++编写代码,则更推荐使用以下习惯用法

for (;;) { ... }

没有特别的原因,但你应该这样写,因为这是C程序员期望看到的方式。


9
我从未遇到过喜欢使用for(;;;){} 而不是 while(1){}的C程序员。 - Javier
2
如果我没记错的话,K&R更喜欢使用for(;;){}而不是while(1){}。 - Windows programmer
2
我认为使用(;;) { } 而不是 while 的原因是因为它更接近于表达“永远”。 - Roalt
2
我认为使用 #define EVER ;; 更易读,然后你可以说 for(EVER),这使得它更清晰,EVER 是一个定义。 - Its me
12
以防万一,请不要真的定义任何这些宏...... - Ben Burns
显示剩余2条评论

13

我更喜欢

while(!<some condition>) {
    //do something
}

但我认为这更多是关于代码的可读性,而非“忘记break”的潜在问题。我认为忘记使用 break 是一个相当微弱的论点,因为那将是一个错误,你会立即发现并修复它。

我反对使用 break 来结束无限循环的理由是,实际上是把 break 语句作为了一个 goto。我并不是固执地反对使用 goto (如果该语言支持它,那么就可以使用),但如果有更易读的替代方案,我会尝试使用它来代替。

对于许多 break 点的情况,我会用什么来替换它们呢?

while( !<some condition> ||
       !<some other condition> ||
       !<something completely different> ) {
    //do something
}

这种方式将所有终止条件整合在一起,使得更容易看到是什么将结束这个循环。如果散布break语句的话,那就难以阅读了。


或者如果您觉得使用太多OR不够优雅,您可以在编译单元中定义一个名为we_are_good()的函数{ return cond1 || cond2 || cond3; },并将该谓词函数用作循环条件。 - wilhelmtell
通常我只会把它们提取到函数中,如果我将使用该功能超过一次,但那只是我的个人偏好问题。可读性也是这么做的一个很好的理由。 - Bill the Lizard

11

如果您有许多语句并且希望在任何一个失败时停止,那么使用while (true)可能是有意义的。

 while (true) {
     if (!function1() ) return;   
     if (!function2() ) return;   
     if (!function3() ) return;   
     if (!function4() ) return;  
   }

优于

 while (!fail) {
     if (!fail) {
       fail = function1()
     }           
     if (!fail) {
       fail = function2()
     }  
     ........

   }

1
是的,这正是我的经验。我认为前者更容易阅读。 - Edward Tanguay
1
在我的大学里,有一位老师告诉我,在一个函数中使用多个返回语句是绝对不好的。你应该将结果放入一个变量中(例如称为result),并在最后返回它。当然,我没有听从他的建议... - Roalt
1
@Roalt - 请查看https://dev59.com/PXVD5IYBdhLWcg3wQZUg - Daniel Earwicker
2
再说,这四个if语句可以被重写并移到while条件中: while (function1() && function2() ...) - Thomas Eyde
while(true)多重退出确实可以让你在函数调用之间放置其他代码。 - Martin Beckett
我建议使用以下代码结构:while(isBreakConditionGiven()) {..},并将所有的 if (!function1() ) return; if (!function2() ) return; if (!function3() ) return; if (!function4() ) return; 放入 isBreakConditionGiven() 方法中。这样做可以使代码更加清晰易读,;-) - Gizzmo

8

哈维尔在我早期的答案中(引用Wordsworth的答案)发表了一个有趣的评论:

我认为while(true) {}比while(condition){}更具“纯度”。

我无法在300个字符内做出充分回应(抱歉!)

在我的教学和指导中,我非正式地将“复杂性”定义为“我需要记住多少其他代码才能理解这一行或表达式?” 我需要记住的东西越多,代码就越复杂。 代码告诉我的东西越明确,复杂度就越低。

因此,为了降低复杂性,让我从完整性和强度而非纯度的角度回答哈维尔。

我认为这段代码片段:

while (c1) {
    // p1
    a1;
    // p2
    ...
    // pz
    az;
}

同时表达两个事情:

  1. 只要保持为真,整个循环体将会一直重复执行;且
  2. 在执行时刻1,保证为真。

这两种方式的区别在于视角不同;第一种与整个循环的外部、动态行为有关,而第二种有助于理解我在思考特定的时刻1时可以依赖的内部、静态保证。当然,的净效应可能会使失效,需要我更加深入地思考我在时刻2可以依赖什么等。

让我们放置一个具体的(微小)示例来思考条件和第一步操作:

while (index < length(someString)) {
    // p1
    char c = someString.charAt(index++);
    // p2
    ...
}

“外部”问题是,循环显然在someString内部执行某些只有在index定位在someString中时才能完成的操作。这种情况下,我们期望会在循环体内(位置和方式在检查循环体之前是未知的)修改indexsomeString,以便最终终止循环。这为思考循环体提供了背景和期望。
“内部”问题是,在点1后面的操作是合法的,因此在阅读点2处的代码时,可以考虑使用已经被合法获得的char值来做什么。(如果someString为空引用,我们甚至不能评估条件,但我还假设我们在这个示例的上下文中已经进行了保护!)
相比之下,以下形式的循环:
while (true) {
    // p1
    a1;
    // p2
    ...
}

让我对两个问题都感到失望。在外部层面上,我不确定这是否意味着我应该期望这个循环永远循环下去(例如操作系统的主事件分派循环),还是有其他事情发生。这使我既没有阅读正文的明确上下文,也没有关于何时结束的进展预期。

在内部层面上,我绝对没有任何关于可能在点1处发生的任何情况的明确保证。条件“true”,当然在任何地方都是真的,是我们可以在程序中任何时候知道的最弱陈述。了解行动的前提条件是非常有价值的信息,当试图思考行动所完成的内容时!

因此,根据我上面描述的逻辑,我建议while (true) ...习惯用法比while (c1) ...更加不完整和薄弱,因此更加复杂。


8

问题在于,并非所有的算法都遵循“while(cond){action}”模型。

通用的循环模型如下:

loop_prepare
 loop:
  action_A
  if(cond) exit_loop
  action_B
  goto loop
after_loop_code

当没有 action_A 时,您可以使用以下替代方法:

loop_prepare
  while(cond)
  action_B
after_loop_code

当没有action_B时,你可以用以下内容替换它:
loop_prepare
  do action_A
  while(cond)
after_loop_code

在一般情况下,action_A将被执行n次,而action_B将被执行(n-1)次。
一个真实的例子是:打印由逗号分隔的表格中的所有元素。我们希望有n个元素和(n-1)个逗号。
你总是可以使用一些技巧来坚持while循环模型,但这会重复代码或检查每个循环的相同条件,或添加一个新变量。因此,你总是比while-true-break循环模型更低效和不易读懂。
(坏)“技巧”的例子:添加变量和条件。
loop_prepare
b=true // one more local variable : more complex code
 while(b): // one more condition on every loop : less efficient
  action_A
  if(cond) b=false // the real condition is here
  else action_B
after_loop_code

一个(糟糕的)“技巧”的例子:重复代码。在修改两个部分之一时,不能忘记重复的代码。

loop_prepare
action_A
 while(cond):
  action_B
  action_A
after_loop_code

注意:在最后一个例子中,程序员可以通过将“loop_prepare”与第一个“action_A”混合,并将action_B与第二个action_A混合来混淆(有意或无意)代码。因此,他可能会感觉自己并没有这样做。


4
如果有多种方法可以跳出循环,或者如果无法在循环顶部轻松表达中断条件(例如,循环内容需要运行一半但另一半不必运行,在最后一次迭代中),那么第一种方法是可行的。但是如果可以避免,就应该避免,因为编程应该是以最明显的方式编写非常复杂的事物,同时正确地实现功能和性能。这就是为什么你的朋友在一般情况下是正确的。假设前面段落中描述的条件不成立,你朋友编写循环结构的方式更加明显易懂。

3

在SO上已经有一个基本相同的问题,链接为Is WHILE TRUE…BREAK…END WHILE a good design?。@Glomek给出了一个被低估的回答:

有时候这是非常好的设计。参见Donald Knuth的Structured Programing With Goto Statements一书中的一些例子。我经常使用这个基本思想来运行“n个半”循环,特别是读取/处理循环。然而,我通常只使用一个break语句。这样可以更容易地推理出循环终止后程序的状态。

稍后,我发表了相关但同样被低估的评论(部分原因是我第一次没有注意到Glomek的回答):

一个非常有趣的文章是Knuth在1974年发表的“带有go to语句的结构化编程”(可在他的书籍“文学编程”中找到,也可能其他地方有)。它讨论了控制循环退出的方法,以及(没有使用术语)循环和半个语句等内容。 Ada还提供了循环结构,包括
loopname:
    loop
        ...
        exit loopname when ...condition...;
        ...
    end loop loopname;

这个问题的代码意图与此类似。

参考的 SO 项和这个问题之间的一个区别是“最终中断”;这是一个单次循环,使用 break 提前退出循环。关于这是否也是一种良好的风格,曾经有过一些问题 - 我手头没有交叉参考。


3

有时候你需要无限循环,例如监听端口或等待连接。

因此,while(true)... 不应被归类为好或坏,让情况决定使用什么。


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