有必要使用“do {...} while()”循环吗?

92
Bjarne Stroustrup(C++ 创造者)曾经说过,他避免使用“do/while”循环,并更喜欢用“while”循环来编写代码。[请参见下面的引用语句。]
自从听到这个观点以来,我发现这是真的。您有什么想法?是否有一个例子,其中“do/while”比使用“while”更清晰易懂?
针对一些答案的回应:是的,我了解“do/while”和“while”之间的技术差异。这是关于可读性和循环结构代码的更深层次的问题。
让我换一种方式问:假设您被禁止使用“do/while”,是否有一个现实的例子,这将使您别无选择,只能使用“while”编写不干净的代码?
来自《C++程序设计语言》6.3.3:
在我的经验中,do-语句是错误和混乱的源头。原因是它的主体总是在条件被评估之前执行一次。但是,为了使主体正常工作,即使第一次通过时,也必须像条件一样保持某种程度的正确性。我发现,在程序首次编写和测试或之后修改其前面的代码后,该条件通常不会按预期保持。我还更喜欢将条件“放在我能看到的地方”。因此,我倾向于避免使用 do-语句。——Bjarne 避免使用do/while循环是C++核心准则中的一项建议,即ES.75,避免使用do语句

3
"Code Review"在2009年6月还不存在。你的意思是什么? - Mast
1
@brandaemon 这是一个非常糟糕的建议... - Ismael Miguel
3
@brandaemon建议在StackOverflow或Programmers.SE上发问。Code Review仅适用于已完全工作的代码,并且必须包括代码。这个问题没有任何代码,这使得它不适合在Code Review上讨论。 - Ismael Miguel
那很有道理。我会这样做的。 - intcreator
5
@brandaemon 请查看《针对Stack Overflow用户的代码审查指南》 - Mathieu Guindon
1
@brandaemon 我建议你花点时间阅读这篇元帖,它对这个主题进行了广泛的探讨。它提供了很多关于Programmers.StackExchange上哪些问题是适合讨论的见解。希望这也能帮到你! - enderland
20个回答

108

我同意do while循环可以改写成while循环,但我不同意总是使用while循环更好的说法。do while循环总是至少运行一次,这是非常有用的属性(最典型的例子是输入检查(来自键盘))。

#include <stdio.h>

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

当然可以将这个代码重写为while循环,但通常情况下这被认为是一种更加优雅的解决方案。


3
这是“while”版本:char c = -1; while (c < '0' || c > '9') { printf(...); scanf(...); } - Dustin Boswell
54
@Dustin,依我之见,你的反例使用了魔数(-1),因此在可读性方面不如原始代码更佳。所以就“更好”的代码(当然这是主观的)而言,我认为@Rekreativc的原始代码更好,因为更容易被人类眼睛读懂。 - Ron Klein
9
我不确定我会称它为“神奇”数字,但无论如何,如果更清晰的话,您可以将其初始化为“char c ='0'- 1;”。也许你真正抵制的是'c'实际上有一个初始值的事实?我同意这有点奇怪,但另一方面,“while”循环很好,因为它提前声明了继续条件,所以读者立即就能理解循环的生命周期。 - Dustin Boswell
6
我的反对意见是,在原则上,“c”根本不应该有一个可观察的初始值。这就是为什么我更喜欢使用do-while版本。(尽管我认为你关于现实世界可读性的观点也有道理。) - mqp
9
@Dustin:你重写的一个重要缺陷是需要从char的域中保留一个元素(这里是-1)作为特殊标记。虽然这不影响此处的逻辑,但通常意味着您无法再处理任意字符流,因为它可能包含具有值-1的字符。 - j_random_hacker
显示剩余7条评论

38

do-while是一种带有后置条件的循环。在循环体至少需要执行一次的情况下,需要使用它。这对于需要在循环条件可以被有意义地评估之前执行某些操作的代码是必要的。使用while循环,您将不得不从两个位置调用初始化代码,而使用do-while,您只能从一个位置调用它。

另一个例子是当您在第一次迭代开始时已经拥有一个有效对象,因此您不想在第一次迭代开始之前执行任何操作(包括循环条件评估)。一个示例是FindFirstFile/FindNextFile Win32函数:您调用FindFirstFile,它会返回一个错误或指向第一个文件的搜索句柄,然后您调用FindNextFile,直到它返回一个错误。

伪代码:

Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
   do {
      process( params ); //process found file
   } while( ( handle = FindNextFile( params ) ) != Error ) );
}

8
一个前置条件吗?如果在执行主体后检查条件,那不就成了后置条件了吗? :) - Ionut Anghelcovici
8
这是我认为更易读且嵌套较少的重写:Handle handle = FindFirstFile(params); while(handle != 错误) { 处理(params); handle = FindNextFile(params); } - Dustin Boswell
你能否使用while循环完全避免if判断呢? while( ( handle = FindNextFile( params ) ) != Error ) ) { process( params ); } 我的意思是,if语句中的条件与循环中的条件完全相同。 - Juan Pablo Califano
4
@Dustin:它也可以这样做(而且可能应该这样做)for(handle = FindFirstFile(params); handle != Error; handle = FindNextFile(params)) {} - falstro
我认为这个答案比被接受的答案更好,因为你明确地说过,你必须从两个站点进行初始化,一次在while循环之前,一次在while循环内部。这确实可以通过使用do-while解决。当我遇到这种情况时,我发现自己使用do-while编写更好、更易读的代码。你只需要在循环之前声明变量,但不初始化它们。所以,没有重复的代码。对我来说,这是do-while相对于while循环最有用的特性。 - Timmos
显示剩余3条评论

18

do { ... } while (0)是一个重要的结构,用于使宏表现良好。

即使在实际代码中这并不重要(我并不一定同意),但对于修复预处理器的一些缺陷非常重要。

编辑:今天我在自己的代码中遇到了一种情况,使用do/while会更加简洁明了。我正在创建一个平台无关的LL/SC指令的成对抽象。这些指令需要在循环中使用,如下所示:

do
{
  oldvalue = LL (address);
  newvalue = oldvalue + 1;
} while (!SC (address, newvalue, oldvalue));

(专家们可能意识到,在SC实现中,oldvalue没有被使用,但它包含在内是为了可以使用CAS来模拟这个抽象。)

LL和SC是一个很好的例子,其中do/while形式比等效的while形式更加简洁明了:

oldvalue = LL (address);
newvalue = oldvalue + 1;
while (!SC (address, newvalue, oldvalue))
{
  oldvalue = LL (address);
  newvalue = oldvalue + 1;
}

因此,我非常失望谷歌Go语言决定删除do-while结构


为什么宏需要这个?你能举个例子吗? - Dustin Boswell
5
请看http://c2.com/cgi/wiki?TrivialDoWhileLoop。预处理器宏并不会“处理”C语法,因此需要像这样聪明的技巧来防止宏在没有花括号的情况下失效,例如在一个if语句中。 - Wesley
1
在网上有很多关于这个问题的详细解释,但是我在谷歌搜索了几分钟也没有找到一个全面的链接。最简单的例子就是它可以使多行宏表现为单行,这很重要,因为宏的使用者可能会在未加括号的单行for循环中使用它,如果没有正确包装,这种用法将会出错。 - Dan Olson
我认为在 #define 中使用 do while 循环是令人讨厌的。这也是预处理器名声不佳的原因之一。 - Pod
1
你应该澄清一下 Pod,是因为你不喜欢它的美学风格,还是因为某些原因不喜欢额外的安全性,或者你认为这并不必要?帮我理解一下。 - Dan Olson
我认为有必要向您展示预处理器的危险性。 - Martin Beckett

8
以下这个常见的习语对我来说似乎非常简单明了:
do {
    preliminary_work();
    value = get_value();
} while (not_valid(value));

重写以避免使用 do 的方法似乎是:
value = make_invalid_value();
while (not_valid(value)) {
    preliminary_work();
    value = get_value();
}

第一行代码是用来确保测试在第一次执行时总是评估为真。换句话说,第一次测试总是多余的。如果没有这个多余的测试,也可以省略初始赋值。这段代码给人的印象是它在和自己斗争。

在这种情况下,do结构是一个非常有用的选项。


7

当你想要在满足某个条件之前一直“执行”某些操作时,它非常有用。

可以通过while循环来模拟实现:

while(true)
{

    // .... code .....

    if(condition_satisfied) 
        break;
}

1
实际上,如果“... code ...”包含一个'continue'语句,这与“do { ... code ... } while (condition)”是不等价的。'continue'的规则是在do/while循环中跳到底部。 - Dustin Boswell
好的观点,不过我说的是“fudge”,这意味着它有点不对。 - hasen

6

在我们的编码规范中

  • if / while / ... 条件语句 不具有副作用,且
  • 变量必须进行初始化。

因此,我们几乎从不使用 do {} while(xx) 循环。因为:

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

被重写为:

int main() {
    char c(0);
    while (c < '0' ||  c > '9');  {
        printf("enter a number");
        scanf("%c", &c);
    } 
}

并且。
Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
   do {
      process( params ); //process found file
   } while( ( handle = FindNextFile( params ) ) != Error ) );
}

被重写为:

Params params(xxx);
Handle handle = FindFirstFile( params );
while( handle!=Error ) {
    process( params ); //process found file
    handle = FindNextFile( params );
}

7
看到你第一个例子中的错误了吗?也许使用do-while循环可以帮助你避免那个错误。 :-) - Jouni K. Seppänen
3
我讨厌看带有do...while循环的代码。这就好像有人给你详细介绍一些你完全不关心的东西,然后在唠叨了15分钟之后,告诉你为什么你应该一开始就关心它。让代码易读。直接告诉我循环的目的,不要让我无缘无故地滚动屏幕。 - Kieveli
@Kieveli - 我也有同样的想法。我认为程序员习惯于在顶部设置条件,就像在“for”和“if”中一样。do/while确实是一个奇怪的东西。 - Dustin Boswell
4
大多数程序员似乎对在中间使用条件 (if (...) break;) 没有问题,甚至经常与 while(true)for(;;) 结合使用。那么是什么让 do ... while 更糟糕呢?至少你知道在哪里寻找条件。 - celtschk
1
@celtschk 我之前的所有工作场所都认为 if (bCondition) break; 是一种不好的设计,并鼓励使用适当的循环条件。当然,有一些情况下无法避免使用 breakcontinue,但大多数情况下,它们是懒惰编码的标志。 - mg30rg

6

这是我见过的最清晰的 do-while 替代方案。它是 Python 推荐的习惯用法,因为 Python 没有 do-while 循环。

需要注意的一点是,在 <setup code> 中不能使用 continue,因为这会跳过 break 条件,但是在条件之前没有任何例子需要在此之前使用 continue。

while (true) {
       <setup code>
       if (!condition) break;
       <loop body>
}

这里应用了一些最好的do-while循环的示例。
while (true) {
    printf("enter a number");
    scanf("%c", &c);
    if (!(c < '0' ||  c > '9')) break;
} 

下面这个例子中,由于条件通常很短,而“//获取数据”保持在顶部附近,所以结构比do-while更易读。而“//处理数据”部分可能会很长。
while (true) {
    // get data 
    if (data == null) break;
    // process data
    // process it some more
    // have a lot of cases etc.
    // wow, we're almost done.
    // oops, just one more thing.
} 

很好的反例;使用 break - Mr. Polywhirl

6
首先,我同意 do-whilewhile 更难读懂。
但是令我惊讶的是,在这么多答案中,没有人考虑过为什么语言中甚至存在 do-while。原因是效率。
假设我们有一个带有 N 个条件检查的 do-while 循环,其中条件的结果取决于循环体。如果我们将其替换为 while 循环,我们将获得 N+1 个条件检查,其中额外的检查是无意义的。 如果循环条件只包含整数值的检查,那么这并不重要,但是假设我们有:
something_t* x = NULL;

while( very_slowly_check_if_something_is_done(x) )
{
  set_something(x);
}

循环的第一次调用函数是多余的:我们已经知道x还没有被设置。那么为什么要执行一些无意义的开销代码呢?

当我编写实时嵌入式系统时,我经常使用do-while来达到这个目的,因为条件内部的代码相对较慢(检查一些缓慢的硬件外设的响应)。


2
在while循环中添加一个检查while (x == null || check(x)) . . .可以解决这个问题。 - D.B. Fred
3
@D.B.Fred,这样做的性能仍然不够好,因为你可能会在循环中的每一圈都添加一个额外的分支。 - Lundin

6

假设您知道两者之间的区别

在检查条件并运行while循环之前,Do/While循环适用于引导/预初始化代码。


5
这一切都关乎“易读性”。更易读的代码能够减少维护时的头痛,促进更好地协作。在大多数情况下,其他考虑因素(如优化)要远不如易读性重要。我将详细说明,因为这里有一个评论:
如果你有一个使用do { ... } while()的代码片段A,它比其等价的while() {...}代码片段B更易读,则我会投票支持A。如果你更喜欢B,因为你可以看到循环条件“up front”,并且你认为它更易读(因此更易于维护等),那就用B吧。
我的观点是:使用对你和你的同事来说更易读的代码。当然,选择是主观的。

1
没有人怀疑,尤其是原帖作者。这绝对不能回答问题。 - codymanix

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