当(真)用破坏坏的编程实践?

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

while(true) {


//do something


if(<some condition>) {
break;
}


}

另一个程序员告诉我,这是一个不好的做法,我应该用更标准的方法来替代它:

while(!<some condition>) {


//do something


}

他的理由是,你可以很容易地“忘记休息”,并有一个无休止的循环。我告诉他,在第二个例子中,你可以很容易地放入一个条件,这个条件永远不会返回 true,所以很容易就会有一个无穷无尽的循环,所以两者都是同样有效的实践。

此外,我通常更喜欢前者,因为当您有多个断点时,它使代码更容易阅读,也就是说,多个不在循环中的条件。

有人能通过为一方或另一方添加证据来丰富这一论点吗?

101000 次浏览

如果有很多方法可以中断循环,或者中断条件不能在循环顶部轻松表达(例如,在最后一次迭代中,循环的内容需要运行一半,但另一半不能运行) ,那么第一种方法是可以的。

但是如果可以避免这种情况,那么就应该避免,因为编程应该尽可能以最明显的方式编写非常复杂的内容,同时正确且高效地实现特性。这就是为什么你的朋友,在一般情况下,是正确的。您的朋友编写循环结构的方法要明显得多(假设前一段描述的条件没有获得)。

这取决于你想要做什么,但是一般来说,我更喜欢把条件句放在 while 中。

  • 它更简单,因为您不需要在代码中进行另一个测试。
  • 它更容易阅读,因为您不必在循环中寻找突破口。
  • 你是重造轮子。While 的全部意义在于,只要测试为真,就可以执行某些操作。为什么要通过将中断条件放在其他地方来破坏它呢?

如果我正在编写一个守护进程或其他进程,那么我会使用 while (true)循环,该进程应该一直运行直到被杀死。

他可能是对的。

在功能上,两者可以是相同的。

但是,对于 可读性和理解程序流程,while (条件)更好。骨折更像是一种痛苦。While (条件)对继续循环的条件非常清楚,等等。这并不意味着中断是错误的,只是可读性较差。

这两个例子之间有一个矛盾。第一个将每次至少执行一次“ do something”,即使该语句从未为真。第二种方法只有在语句的计算结果为 true 时才会“做一些事情”。

我想你要找的是 do-while 循环。我100% 同意 while (true)不是一个好主意,因为它使得维护这段代码变得很困难,而且你逃避循环的方式非常 goto风格,这被认为是不好的做法。

试试:

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

检查你的个人语言档案编制的确切语法。但是看看这段代码,它基本上执行 do 中的操作,然后检查 while 部分,看看是否应该再执行一次。

我想到了使用后一种结构的一些优点:

  • 在不查找循环代码中的中断的情况下,更容易理解循环在做什么。

  • 如果在循环代码中没有使用其他中断,那么在循环中只有一个退出点,那就是 while ()条件。

  • 通常结果是代码更少,这增加了可读性。

我更喜欢 while (!)方法,因为它更清楚、更直接地表达了循环的意图。

我更喜欢

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

但我认为这更多的是一个可读性的问题,而不是潜在的“忘记休息”我认为,忘记 break是一个相当弱的论点,因为这将是一个错误,你会发现并立即修复它。

我反对使用 break来走出无穷无尽的循环的理由是,实际上您将 break语句作为 goto使用。我并不是非常反对使用 goto(如果语言支持它,那就是合理的选择) ,但是如果有更可读的替代品,我会尝试替换它。

在很多 break点的情况下,我会用

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

通过这种方式整合所有的停止条件,可以更容易地看到将要结束这个循环的内容。break语句可能会随处可见,但这些语句根本不具有可读性。

如果有一个(而且只有一个)非异常中断条件,则最好将该条件直接放入控制流构造(while)中。看到 while (true){ ... }让我作为一个代码阅读器认为没有简单的方法来枚举中断条件,并且让我认为“仔细看看这个,仔细想想中断条件(在当前循环中在它们之前设置了什么,以及在前一个循环中可能设置了什么)”

简而言之,在最简单的情况下,我和你的同事在一起,但是(true){ ... }并不罕见。

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()
}
........


}

当您可以在表单中编写代码时

while (condition) { ... }

或者

while (!condition) { ... }

对于 在体内没有出口(ABC0、 ABC1或 goto),这种形式是首选的,因为有人可以阅读代码和 了解终止条件,只需看看标题。这很好。

但是很多循环不适合这个模型,而 中间有显式出口的无限循环是一种可敬的模式。(使用 continue的循环通常比使用 break的循环更难理解。)如果你想要引用一些证据或权威,只要看看 Don Knuth 关于 结构化编程的著名论文就可以了; 你会找到所有你想要的例子、论点和解释。

一个小小的习惯用法: 编写 while (true) { ... }会让你觉得自己是一个老的 Pascal 程序员,或者现在可能是一个 Java 程序员。如果你正在写 在 C 或 C + + 中,首选的习惯用法是

for (;;) { ... }

这样做没有什么好的理由,但是您应该这样编写,因为这是 C 程序员期望看到的方式。

完美的顾问答案是: 视情况而定。大多数情况下,正确的做法是要么使用 while 循环

while (condition is true ) {
// do something
}

或者使用类似 C 语言的

do {
// do something
} while ( condition is true);

如果这两种情况中的任何一种有效,就使用它们。

有时候,比如在服务器的内部循环中,您真正的意思是程序应该一直运行,直到外部中断为止。(考虑一个 httpd 守护进程——除非它崩溃或者因为关闭而停止,否则它不会停止。)

使用 while (1) :

while(1) {
accept connection
fork child process
}

最后一种情况是在终止之前需要执行某些函数的罕见情况。在这种情况下,使用:

while(1) { // or for(;;)
// do some stuff
if (condition met) break;
// otherwise do more stuff.
}

这里有很多关于可读性的讨论,它的构造非常好,但是对于所有不固定大小的循环(即。你冒着风险去做。

His reasoning was that you could "forget the break" too easily and have an endless loop.

在 while 循环中,您实际上要求一个进程无限期地运行,除非发生某些事情,如果某些事情没有在某个参数中发生,您将得到您想要的... 一个无限循环。

我认为使用“ while (true)”的好处可能是让多个退出条件更容易编写,特别是当这些退出条件必须出现在代码块的不同位置时。然而,对于我来说,当我不得不模拟运行代码来查看代码如何交互时,可能会出现混乱。

就个人而言,我会尽量避免。原因是,每当我回头看以前编写的代码时,我通常发现我需要弄清楚它何时运行/终止的次数比它实际执行的次数多。因此,必须首先找到“中断”对我来说有点麻烦。

如果需要多个退出条件,我倾向于将确定逻辑的条件重构为一个单独的函数,这样循环块看起来干净并且更容易理解。

引用过去著名的开发商华兹华斯的话:

...
事实上,我们注定要进入的监狱
我们自己,没有监狱; 因此对我来说,
在杂乱的情绪中,被束缚是一种消遣
在十四行诗所描绘的贫瘠的土地上;
如果有些灵魂(他们的需要必须如此)
他们感受到了太多自由的重量,
应该能找到短暂的慰藉,正如我所发现的。

华兹华斯接受了十四行诗的严格要求作为一个解放的框架,而不是作为一个束缚。我认为,“结构化编程”的核心是放弃构建任意复杂的流程图的自由,转而支持易于理解的解放。

我完全同意 有时候提前退出是表达一个动作最简单的方式。然而,我的经验是,当我强迫自己使用最简单的可能的控制结构(并且真正考虑在这些约束条件下进行设计)时,我通常会发现结果是更简单、更清晰的代码。缺点是

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

很容易让 action0action1变得越来越大的代码块,或者添加“只是多一个”测试中断操作序列,直到它变得很难指向一个特定的行并回答这个问题,“在这一点上我知道什么条件?”因此,在不为其他程序员制定规则的情况下,我尽可能在自己的代码中避免使用 while (true) {...}习惯用法。

在真实的时候,打破,结束,是一个好的设计吗?上有一个大致相同的问题。@Glomek 回答(在一篇被低估的文章中) :

有时候设计得很好。参见 Donald Knuth 的 使用 Goto 语句的结构化编程获得一些例子。对于运行“ n 次半”的循环,我经常使用这个基本思想,特别是 read/process 循环。但是,我通常只使用一个 break 语句。这使得在循环终止后推断程序的状态变得更加容易。

过了一会儿,我回复了相关的评论,不幸的是,这些评论也被低估了(部分原因是我没有注意到格洛梅克的第一次评论,我想) :

其中一篇引人入胜的文章是克努特1974年的《结构化编程》(在他的《文学编程》一书中可以找到,或许在其他地方也可以找到)。除了其他内容之外,它还讨论了打破循环的受控方式,以及(不使用术语)循环半语句。

Ada 还提供循环构造,包括

loopname:
loop
...
exit loopname when ...condition...;
...
end loop loopname;

原始问题的代码在意图上与此相似。

被引用的 SO 条目和这个条目之间的一个区别是“ final break”; 这是一个使用 break 提前退出循环的单发循环。有人质疑这是否也是一种好的风格——我手头没有交叉参考资料。

不,这并不坏,因为在设置循环时,您可能并不总是知道退出条件,或者可能有多个退出条件。然而,它确实需要更多的注意来防止无限循环。

您的朋友推荐的代码与您的不同,您自己的代码更类似于

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

它总是至少运行一次循环,而不管条件如何。

但是有些时候休息是完全可以的,就像其他人提到的那样。为了回应你的朋友“忘记休息”的担忧,我经常这样写:

while(true){
// do something
if(<some condition>) break;
// continue do something
}

通过良好的缩进,断点对于第一次读代码的人来说是清晰的,看起来就像在循环的开始或底部断开的代码一样结构化。

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

因此,虽然(真实的) ... 不应该分类为好或坏,让情况决定使用什么

问题不在于 而(真实的)那部分有多糟糕,而在于你必须打破它或者摆脱它这个事实。Break 和 goto 并不是真正可以接受的流控制方法。

我也不明白这有什么意义。即使在一个循环遍历整个程序周期的程序中,您至少可以使用一个布尔值(称为 ) ,或者将其设置为 true,以便在循环中正确地脱离循环,如 while (!辞职) ... 不只是在某个任意点叫休息然后跳出来,

哈维尔对我之前的回答(引用华兹华斯的话)做了一个有趣的评论:

我认为 while (true){}是一个比 while (条件){}更“纯”的结构。

而且我不能用300个字符来充分地回应(对不起!)

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

因此,为了减少复杂性,让我用完整性和力量而不是纯粹性来回答哈维尔的问题。

我想到了这段代码:

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

同时表达两件事:

  1. 只要 c1保持为真,(整个)主体就会重复,并且
  2. 在执行 a1的点1,c1保证

不同之处在于透视图; 第一个透视图通常与整个循环的外部动态行为有关,而第二个透视图则有助于理解内部静态保证,这是我在考虑 a1时可以依赖的。当然,a1的净效应可能会使 c1失效,这就要求我更努力地思考在第2点我能指望什么,等等。

让我们用一个特定的(微小的)例子来考虑条件和第一个操作:

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

“外部”问题是,循环显然是在 someString内做一些只有在 index位于 someString内时才能做的事情。这建立了一个期望,即我们将在主体内修改 indexsomeString(在我检查主体之前不知道的位置和方式) ,以便最终发生终止。这给了我思考身体的背景和期望。

“内部”的问题是,我们可以保证下面第一点的操作是合法的,所以在阅读第二点的代码时,我可以考虑如何使用合法获得的字符值 I 知道。(如果 someString是空引用,我们甚至不能评估这个条件,但是我还假设我们在本例的上下文中已经提防了这一点!)

相比之下,形式的循环:

while (true) {
// p1
a1;
// p2
...
}

在这两个问题上都让我失望。在外部级别,我想知道这是否意味着我 真的应该期望这个循环永远循环(例如,操作系统的主事件分派循环) ,或者是否有其他事情正在发生。这既没有为我提供阅读主体的明确上下文,也没有提供什么构成了(不确定的)终止的进展的预期。

在内部级别,我有 绝对不行明确保证的任何情况下,可能持有的点1。条件 true,当然在任何地方都是真的,是关于我们在程序的任何一点所能知道的最弱的陈述。了解一个行动的前提条件是非常有价值的信息时,试图思考什么行动完成!

因此,根据我上面描述的逻辑,我认为 while (true) ...惯用语比 while (c1) ...更不完整、更弱,因此也更复杂。

使用类似

而(1){ do stuff }

在某些情况下是必要的。如果你做任何嵌入式系统编程(想想微控制器,如 PIC,MSP430,和 DSP 编程) ,那么几乎所有的代码将在一个 while (1)循环。在为 DSP 编写代码时,有时候你只需要一个 while (1){} ,代码的其余部分是一个 interrupt handler (ISR)。

问题是并非每个算法都遵循“ 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-loop 模型,但是这将总是重复代码或检查两次相同的条件(对于每个循环)或添加一个新变量。因此,与 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 _ ready”与第一个“ action _ A”混合,将 action _ B 与第二个 action _ A 混合。这样他就能感觉到自己并没有这么做。