是否需要“做{…} while()”循环?


75

Bjarne Stroustrup(C ++创建者)曾经说过,他避免执行“ do / while”循环,而是更喜欢使用“ while”循环来编写代码。[请参阅下面的引用。]

自从听到这句话以来,我发现这是真的。你怎么认为?有没有一个示例,其中“ do / while”比使用“ while”更清晰,更容易理解?

针对某些答案:是的,我了解“ do / while”和“ while”之间的技术区别。这是一个涉及循环的可读性和结构化代码的更深层问题。

让我问另一种方式:假设您被禁止使用“ do / while”-是否有一个现实的示例,除了使用“ while”编写不干净的代码,别无选择?

来自“ C ++编程语言”,6.3.3:

以我的经验,做陈述是错误和混乱的根源。原因是在评估条件之前,它的主体总是执行一次。但是,为了使身体正常工作,即使是第一次也必须保持这种状态。比起我想像的更多,我发现该条件在首次编写和测试程序时,或者在修改其之前的代码之后,均未达到预期。 我也更喜欢“可以看到它的前面”条件。因此,我倾向于避免执行陈述。-比耶恩


3
@brandaemon代码审查尚未在2009年6月进行。您的意思是什么?
2015年

1
@brandaemon这是一个非常糟糕的建议...
Ismael Miguel

3
@brandaemon StackOverflow或Programmers.SE。代码审查仅适用于完全正常工作的代码,还必须包括代码。这个问题没有任何代码。这使它在“代码审查”中脱节。
伊斯梅尔·米格尔


1
@brandaemon我将花一些时间阅读这篇关于该主题的文章。它提供了有关Programmers.StackExchange上哪些问题的深入见解。希望也能有所帮助!
enderland

Answers:


96

是的,我同意do while循环可以重写为while循环,但是我不同意始终使用while循环会更好。同时总是至少运行一次,这是一个非常有用的属性(最典型的示例是输入检查(通过键盘))

#include <stdio.h>

int main() {
    char c;

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

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

当然,可以将其重写为while循环,但这通常被认为是一种更为优雅的解决方案。


1
这是“ while”版本:char c = -1; 而(c <'0'|| c>'9'){printf(...); scanf(...); }
Dustin Boswell

46
@ Dustin,IMO,由于使用了幻数(-1),因此您的计数器示例可读性较差。因此,就“更好的”代码(当然是主观的)而言,我认为@Rekreativc的原始代码更好,因为它更容易被眼理解。
罗恩·克莱因

8
我不确定是否将其称为“魔术”数字,但是无论如何,您都可以将其初始化为“ char c ='0'-1;”。如果更清楚。也许您真正抵制的事实是“ c”完全具有初始值?我同意这有点怪异,但另一方面,“ while”很不错,因为它预先声明了继续条件,因此读者可以立即理解循环的生命周期。
达斯汀·博斯韦尔

6
我的反对意见是,原则上,“ c”根本不应该具有可观察的初始值。这就是为什么我希望更好地使用do-while版本。(尽管我认为您对现实世界的可读性有所了解。)
mqp

8
@Dustin:重写的重要缺点是您需要保留来自域的一个元素(此处为-1)char作为特殊标记。尽管这并不影响此处的逻辑,但是通常这意味着您不再能够处理任意字符流,因为它可能包含值为-1的字符。
j_random_hacker 2010年

39

do-while是具有后置条件的循环。如果循环主体至少要执行一次,则需要使用它。这对于代码需要一些动作才能明智地评估循环条件是必要的。如果使用while循环,则必须从两个站点调用初始化代码,而使用do-while则只能从一个站点调用初始化代码。

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

伪代码:

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

8
前提条件?如果在执行一次身体之后检查了条件,那不是后置条件吗?:)

8
这是我认为更易读(且嵌套较少)的重写。Handle handle = FindFirstFile(params); while(handle!= Error){process(params); 句柄= FindNextFile(params); }
Dustin Boswell

是的,以防万一您不需要对每种情况进行详尽的错误处理。
Sharptooth

您是否不能避免在此处使用while循环?while((handle = FindNextFile(params))!=错误)){process(params); 我的意思是,if子句中的条件与循环中的条件完全相同。
Juan Pablo Califano

2
@Dustin:也可以这样做(也许应该这样做)for(handle = FindFirstFile(params); handle != Error; handle = FindNextFile(params)) {}
falstro

18

do { ... } while (0) 是使宏表现良好的重要构造。

即使在实际代码中不重要(我不一定同意),对于修复预处理程序的某些缺陷也很重要。

编辑:我遇到了这样的情况:在我自己的代码中,今天做/做得更干净。我正在对成对的LL / SC指令进行跨平台抽象。这些需要循环使用,如下所示:

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

(专家可能会意识到,在SC实现中未使用oldvalue,但已将oldvalue包括在内,以便可以用CAS模拟此抽象。)

LL和SC是以下情况的一个很好的例子:do / while比while形式等效得多:

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

因此,对于Google Go选择删除do-while构造的事实,我感到非常失望。


为什么宏需要这样做?你能给个例子吗?
达斯汀·博斯韦尔

5
看看< c2.com/cgi/wiki?TrivialDoWhileLoop >。预处理器宏不会“处理” C语法,因此,必须采取类似的聪明技巧,以防止宏插入(例如)if不带括号的语句中。
韦斯利

1
网路上有许多详细的原因,但经过数分钟的搜寻,我找不到完整的连结。最简单的示例是,它使多行宏的行为像单行一样,这很重要,因为宏的用户可能会在无括号的单行for循环中使用它,并且如果未正确包装,则此用法将不正确。
丹·奥尔森,2009年

我发现在#define里面做while令人恶心。这是预处理器名称如此糟糕的原因之一。
波德

1
您应该弄清楚Pod,只是在美学上不喜欢它,出于某种原因不喜欢额外的安全性,或者不同意它的必要性?帮我明白
丹·奥尔森

7

当您想要“做”某事直到满足条件时,它很有用。

可以在while循环中进行这样的操作:

while(true)
{

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

    if(condition_satisfied) 
        break;
}

1
实际上,如果“ ...代码...”包含“ continue”语句,则这等效于“在(条件)时执行{...代码...}”。“继续”的规则是执行一次/同时执行一次。
达斯汀·博斯韦尔

好点,但是,我说了“忽悠”,这意味着有点不对头
Hasen

6

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

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


6

在我们的编码约定中

  • 如果/ 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 );
}

6
在第一个示例中看到错误了吗?也许do-while循环会让您避免这种情况。:-)
Jouni K.Seppänen09年

3
我讨厌用do ... while循环阅读代码。好像有人在向您提供您根本不需要关心的细节,然后经过15分钟的亵渎,他们告诉您为什么您应该首先关心它。使代码可读。提前告诉我循环的用途,不要无缘无故地使我滚动。
Kieveli 2009年

@Kieveli-我的想法也是如此。我认为程序员已经习惯了顶部的条件,就像“ for”和“ if”一样。确实,这是一个奇怪的事情。
达斯汀·博斯韦尔

3
大多数程序员似乎对中间的条件(if (...) break;)并没有问题,甚至常常与while(true)resp相结合。for(;;)。那么,使do ... while您至少知道在哪里寻找病情的地方变得更糟?
celtschk

@celtschk我以前所有的工作场所都认为if (bCondition) break;设计不好,并鼓励使用适当的循环条件。当然,也有一些地方是无法避免的,但大部分的时间情况breakcontinue懒惰编码的迹象。
mg30rg 2014年

6

以下常见用语对我来说似乎很简单:

do {
    preliminary_work();
    value = get_value();
} while (not_valid(value));

要避免的重写do似乎是:

value = make_invalid_value();
while (not_valid(value)) {
    preliminary_work();
    value = get_value();
}

第一行用于确保测试始终在第一时间评估为true。换句话说,第一次测试总是多余的。如果不存在此多余的测试,则也可以省略初始任务。这段代码给人的印象是它自己在战斗。

在这种情况下,do构造是非常有用的选择。


5

一切都与可读性有关。
更具可读性的代码减少了代码维护的麻烦,并改善了协作。
到目前为止,在大多数情况下,其他考虑因素(例如优化)的重要性不那么重要。
我会细说了,因为我有一个在这里评论:
如果你有一个代码段使用do { ... } while(),而且它比其更具可读性while() {...}相当于,那么我会投票给一个。如果你喜欢,因为你看到的循环条件“前面”,你认为这是更具可读性(因此维护等) -那么请便,用
我的观点是:使用对您(以及您的同事)而言更易读的代码。当然,选择是主观的。


1
没有人怀疑,尤其是OP。这绝不能回答问题。
codymanix 2009年

4

这是我所见过的最干净的替代方法。这是不带do-while循环的Python建议使用的习惯用法。

一个警告是您不能使用continue<setup code>因为它会跳过中断条件,但是没有任何一个显示do-while好处的示例在此条件之前需要继续。

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;
} 

下一个示例是一种结构,其用法比做起来更易读的情况,因为条件通常保持在顶部附近,//get data通常较短,但该//process data部分可能很长。

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
Polywhirl先生16年

3

我认为这只是个人选择。

大多数时候,您可以找到一种将do ... while循环重写为while循环的方法;但不一定总是如此。同样,有时编写do while循环以适合您所处的上下文可能更合乎逻辑。

如果您从上方看,TimW的答复就说明了一切。我认为第二个带有Handle的控件尤其混乱。


3

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

由于循环主体中包含的所有内容始终可以封装到例程中,因此不必使用while()do {}

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

问题是当“ LoopBody”变长(例如10行)并且您不想为其创建函数时会发生什么。重复那十行很愚蠢。但是在许多情况下,可以对代码进行重组,以避免重复这10行,但仍使用“ while”。
达斯汀·博斯韦尔

1
当然,两次维护相同的代码对于维护非常不好。
Nosredna

是的,当然可以同时使用两个循环选项来实现更简单,更简洁的代码。我的观点是(数十年来)证明这两者并非严格必要。

另一个重写是:for (bool first=true; first || cond; first=false) { ... }。这不会重复代码,但会引入其他变量。
celtschk

2

我几乎不会仅仅因为以下原因而使用它们:

即使循环检查后置条件,您仍然需要在循环中检查该后置条件,以便您不处理后置条件。

采取示例伪代码:

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

从理论上讲听起来很简单,但在现实世界中,结果可能看起来像这样:

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

额外的“如果”检查就不值得IMO了。我发现很少有实例比执行while循环更简洁。YMMV。


这并不能真正回答问题。在您明确指出您可能已经处在不需要运行内部代码的状态的情况下,绝对正确while() {...}的选择将是正确的-while() {...}专门用于需要运行内部代码的情况根据条件编码零次或多次。但是,这并不意味着在没有情况do {...} while()下,您知道在检查条件之前至少需要运行一次代码的情况(请参阅从@david-božjak选择的答案)。
2014年

2

为了回应未知(google)对Dan Olson的回答的问题/评论:

“做{...},而(0)是使宏表现良好的重要构造。”

#define M do { doOneThing(); doAnother(); } while (0)
...
if (query) M;
...

您看到没有时会发生什么do { ... } while(0)吗?它将始终执行doAnother()。


1
不过,您无需为此做任何事情。#define M {one(); 二(); } 就足够了。
Simon H.

1
但是如果(查询)M; 否则printf(“ hello”); 是语法错误。
Jouni K.Seppänen09年

只有你放一个; 在M之后。否则就可以正常工作。
Simon H.

3
但这更糟:这意味着用户在使用宏时必须记住,其中一些宏会扩展为块而不是表达式。如果您当前使用的是一个无效表达式,并且需要将实现更改为需要两个语句的表达式,那么现在它将扩展为一个块,并且您必须更新所有调用代码。宏已经很糟糕了:使用它们的痛苦应该尽可能地在宏代码中,而不是在调用代码中。
史蒂夫·杰索普

@Markus:对于此特定示例,如果do / while在该语言中不可用,则可以将其替换为#define M((void)(doOneThing(),doAnother()))。
史蒂夫·杰索普

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节的最后一段):

根据我的经验,做陈述是错误和混乱的根源。原因是其主体始终在条件测试之前执行一次。但是,为了使身体正常工作,必须在初次运行中保持与最终状态相似的状态。比我预期的更多,我发现这些条件不成立。当我从头开始编写有问题的程序然后进行测试以及更改代码后,都是这种情况。另外,我更喜欢“在前面可以看到它的条件”。因此,我倾向于避免执行陈述。

(注意:这是我翻译的德语版。如果您是英文版的所有者,请随时对其报价进行修改,以使其与原文一致。不幸的是,Addison-Wesley讨厌Google。)

[1] B. Stroustrup: C ++编程语言。第三版。Addison-Wessley,《雷丁》,1997年。


12
“糟糕吗?豪华!当我还是个小伙子时,我们赤手抓住指令指针,并将其拖到我们接下来要执行的代码上”
Steve Jessop 2009年

2
“拖拉?你这些混蛋。我们不得不鞭打奴隶来推我们的指示指针,这非常困难,因为我们是我们自己的奴隶。然后我们的妈妈会殴打我们,让他们睡着破碎的瓶子。”
Kieveli 2009年

@Tobias-感谢您找到报价。我把它放在最初的问题中(摘自本书的英文版)。
达斯汀·博斯韦尔

@Tobias下面是一个示例,说明重写后的版本如何仅在完全清晰的代码中增加了混乱:假设您有一个循环计数器变量,计算循环执行的次数。在您重写的while循环中,这样的计数器应该在loopBody函数内部还是在while循环内部?答案是“取决于”,没有明显的答案。但是在do-while循环中,这没有关系。
Lundin

2

首先,我同意它do-while的可读性不如while

但是令我惊讶的是,经过如此多的回答,没有人考虑过为什么do-while甚至存在于该语言中。原因是效率。

可以说我们有一个do-while带有N条件检查的循环,其中条件的结果取决于循环主体。然后,如果将其替换为while循环,则会获得N+1条件检查,而多余的检查毫无意义。如果循环条件仅包含一个整数值的检查,那没什么大不了的,但是可以说我们有

something_t* x = NULL;

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

然后,循环的第一圈中的函数调用是多余的:我们已经知道它x尚未设置为任何东西。那么为什么要执行一些毫无意义的开销代码呢?

在对实时嵌入式系统进行编码时,我经常使用do-while来实现此目的,在该系统中,条件内的代码相对较慢(检查某些较慢的硬件外围设备的响应)。


向while添加支票while (x == null || check(x)) . . .可解决此问题
DB Fred

@DBFred但这仍然会带来更差的性能,因为您可能会在循环的每个圈中添加一个额外的分支。
伦丁'18

1

考虑这样的事情:

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

碰巧'\ 0'为0,但我希望您明白这一点。


{}在这里比在{{}时更干净吗?
codymanix 2009年

在While {}中,您必须在循环末尾添加代码。
奈夫岑

我认为这不是一个很好的例子。想象一下发生了什么SumOfString("")
quinmars

1

我的do / while问题完全是在C中实现的。由于while关键字的重用,它经常使人绊倒,因为它看起来像是一个错误。

如果while仅保留了while循环,而do / while已更改为do /直到重复/直到,则我认为循环(这当然很方便,并且是编写某些循环的“正确”方法)不会导致这么麻烦

关于JavaScript我以前对此大加赞赏,它也从C继承了这个遗憾的选择。


但是那里的每种主要编码样式都是一致的,它们总是将whileof编写do-while为与}。最后总是有一个分号。没有C程序员至少有能力的微微有点迷糊或“绊倒”遇到线的时候:} while(something);
Lundin

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
}

但是同样,可读性下降,更不用说在条件内使用赋值语句被认为是不好的做法,我只是想指出,执行赋值语句比分配“保留”值来指示要紧凑得多循环到它是最初运行的过程。


0

我喜欢DavidBož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程序员)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.