有必要使用“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个回答

3

阅读结构化程序定理。 do{} while() 可以总是被重写为 while() do{}。序列、选择和迭代是所有需要的。

由于循环体中包含的任何内容都可以封装到一个例程中,因此使用 while() do{} 的不干净永远不会变得更糟。

LoopBody()
while(cond) {
    LoopBody()
}

问题是当“LoopBody”变得很长(比如10行),而且你不想为它创建一个函数时会发生什么。重复这10行是愚蠢的。但在许多情况下,代码可以重新结构化以避免重复这10行,但仍然使用“while”。 - Dustin Boswell
1
当然了,同样的代码出现两次对于维护来说是相当糟糕的。 - Nosredna
是的,当然拥有两种循环选项可以使代码更容易、更清晰。我的观点是,几十年来已经证明了两者并不是绝对必要的。 - user447688
另一种重写方式是:for (bool first=true; first || cond; first=false) { ... }。这不会复制代码,但会引入一个额外的变量。 - celtschk

3

在我看来,这只是个人选择。

大多数情况下,你可以找到一种方法将 do ... while 循环重写为 while 循环;但并不总是如此。有时为了适应你所处的上下文,编写 do while 循环可能更具逻辑意义。

如果你看一下上面 TimW 的回复,它本身就说明了问题。特别是 Handle 的回复,在我看来更加混乱。


2

do-while循环可以总是重写为while循环。

是否只使用while循环,或者while、do-while和for循环(或任意组合),很大程度上取决于您对美学的喜好以及您所工作的项目的惯例。

个人而言,我更喜欢使用while循环,因为它可以简化关于循环不变量的推理。

至于是否存在需要使用do-while循环的情况:可以用以下代码替代:

do
{
  loopBody();
} while (condition());

您可以随时

loopBody();
while(condition())
{
  loopBody();
}

所以,如果没有特殊原因,你永远不需要使用do-while。 (当然,这个例子违反了DRY,但它只是一个概念证明。在我的经验中,通常有一种方法可以将do-while循环转换为while循环,并且在任何具体用例中都不会违反DRY。)
“入乡随俗。”
顺便说一句:你可能正在寻找的引用是这个([1],第6.3.3节的最后一段):
从我的经验来看,do语句是错误和混淆的源泉。原因是在测试条件之前,它的主体总是被执行一次。然而,为了正确地运行主体,在第一次运行时必须满足类似于最终条件的条件。我预计发现这些条件并不总是真实的。这种情况发生在我从头开始编写程序并进行测试以及在更改代码后。此外,我更喜欢“up-front”的条件,因为我可以看到它。因此,我倾向于避免使用do语句。
(注意:这是我对德语版的翻译。如果你恰好拥有英文版,请随意编辑引用以匹配他的原始措辞。不幸的是,Addison-Wesley讨厌谷歌。)
[1] B. Stroustrup:“C ++编程语言”。第3版。 Addison-Wessley,Reading,1997年。

13
"gotos?奢侈品!在我小时候,我们用光着手抓住指令指针,并将其拖到我们想要执行的代码上。" - Steve Jessop
3
拖拽?你们这些幸运的混蛋。我们不得不鞭打奴隶来推动我们的指令指针,这非常困难,因为我们自己就是奴隶。然后我们的妈妈会用破瓶子把我们打昏。 - Kieveli
@Tobias - 感谢您找到这个引用。我已将其放入原始问题中(取自我所拥有的英文版书籍)。 - Dustin Boswell
@Tobias 这是一个例子,说明重写后的代码只会让原本清晰易懂的代码变得更加混乱:假设你有一个循环计数器变量,用于计算循环已经执行了多少次。在你重写的 while 循环中,这样的计数器应该放在 loopBody 函数内部还是 while 循环内部?答案是“取决于情况”,没有明显的答案。但在 do-while 循环中,这并不重要。 - Lundin

2

我很少使用它们,原因如下:

即使循环检查后置条件,你仍然需要在循环内部检查这个后置条件,以便不会处理后置条件。

看一下这个伪代码示例:

do {
   // get data 
   // process data
} while (data != null);

理论上听起来很简单,但在实际情况中可能会变得更复杂:

do {
   // get data 
   if (data != null) {
       // process data
   }
} while (data != null);

在我看来,额外的“if”检查并不值得。我发现很少有情况比使用do-while循环更简洁。具体情况可能因人而异。


这并没有真正回答问题。在你特别指出的情况下,你可能已经处于不需要运行内部代码的状态,那么 while() {...} 显然是适当的选择 - while() {...} 旨在处理基于条件运行零次或多次代码的情况。然而,这并不意味着永远不会使用 do {...} while(),有些情况下你知道在检查条件之前代码至少需要运行一次(请参见 @david-božjak 的选定答案)。 - Chaser324

2
从未知 (谷歌) 对 Dan Olson 的答案的一个问题/评论中得出:
"do { ... } while (0) 是使宏表现良好的重要构造。"
#define M do { doOneThing(); doAnother(); } while (0)
...
if (query) M;
...

你看到了没有使用 do { ... } while(0) 会发生什么?它将始终执行 doAnother()。


1
你不需要使用do while循环来完成这个任务。只需使用#define M { one(); two(); }即可。 - Simon H.
2
但是如果(查询)M; else printf("hello"); 是语法错误。 - Jouni K. Seppänen
只有在 M 后面加上 ; 才能正常工作,否则它就无法正常工作。 - Simon H.
4
但这更糟:这意味着用户在使用您的宏时必须记住其中一些会扩展为一个块而不是表达式。如果您当前有一个空白表达式的宏,并且您需要将其实现更改为需要两个语句的内容,那么现在它会扩展为一个块,您必须更新所有调用代码。宏已经够糟糕了:尽可能多地减少使用它们的痛苦应该是在宏代码中而不是调用代码中。 - Steve Jessop
@Markus:对于这个特定的示例,如果语言中没有可用的do/while,则可以使用#define M ((void) (doOneThing(), doAnother()))来替换它。 - Steve Jessop
那些在语句后始终编写 {} 的明智做法的人不需要像这样的晦涩宏。我一直认为这些 do-while 宏是混淆代码的。但当然,问题的真正根源在于首先使用了类似函数的宏。 - Lundin

1

我喜欢David Božjak的例子。然而,作为抛砖引玉,我认为你可以将想要运行至少一次的代码因式分解成一个单独的函数,从而实现可能是最易读的解决方案。例如:

int main() {
    char c;

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

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

可能变成这样:

int main() {
    char c = askForCharacter();
    while (c < '0' ||  c > '9') {
        c = askForCharacter();
    }
}

char askForCharacter() {
    char c;
    printf("enter a number");
    scanf("%c", &c);
    return c;
}

(请原谅任何不正确的语法;我不是C程序员)


1
也许这需要回到之前的几个步骤,但在这种情况下。
do
{
     output("enter a number");
     int x = getInput();

     //do stuff with input
}while(x != 0);

虽然可能不易阅读,但可以使用

int x;
while(x = getInput())
{
    //do stuff with input
}

现在,如果您想使用除0以外的数字来退出循环。
while((x = getInput()) != 4)
{
    //do stuff with input
}

但是,再次强调,这样做会损失可读性,更不用说在条件语句内使用赋值语句被认为是不好的实践。我只是想指出,有比将“保留”值分配给循环以指示它是初始运行更紧凑的方法。


1

考虑类似这样的内容:

int SumOfString(char* s)
{
    int res = 0;
    do
    {
        res += *s;
        ++s;
    } while (*s != '\0');
}

碰巧'\0'是0,但我希望你能理解我的意思。


使用do{}while比while{}更加简洁,您同意吗? - codymanix
在 While{} 循环中,您需要在循环结束时添加代码。 - Nefzen
1
我不认为这是一个好的例子。想象一下 SumOfString("") 会发生什么。 - quinmars

1

我对C语言中的do/while实现存在问题。由于重复使用while关键字,它经常会让人们感到困惑,因为它看起来像是一个错误。

如果while仅用于while循环,而do/while被改成do/untilrepeat/until,我认为这种循环(肯定很方便,也是编写某些循环的“正确”方式)不会引起太多麻烦。

我之前曾在JavaScript中抱怨过这个问题, 这也是从C语言继承来的不幸选择。


但是,所有主要的编码风格都是一致的,它们总是将 whiledo-while 写在与 } 相同的行上。并且在结尾处总是有一个分号。没有任何具有至少一点能力的 C 程序员会在遇到 } while(something); 这一行时感到困惑或“绊倒”。 - Lundin

0
我在一个代码库中找到了一个相当不错的例子,其中涉及到页面令牌。
object pageToken = null;
do
{
    var request = new Request(someArgs, pageToken);
    var result = EndPoint(request);
    ...............................
    pageToken = result.NextPageToken;
}
while (pageToken != null)

ps:虽然这篇文章是关于C++的,但是因为上下文相关,所以使用了C#,请谅解 :P


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