【问题标题】:Is there ever a need for a "do {...} while ( )" loop?是否需要“do {...} while ( )”循环?
【发布时间】:2010-11-02 22:53:43
【问题描述】:

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

自从听到这个,我发现这是真的。你都有些什么想法呢?有没有一个例子表明“do/while”比使用“while”更简洁、更容易理解?

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

让我问另一种方式:假设你被禁止使用“do/while” - 有没有一个现实的例子让你别无选择,只能使用“while”编写不干净的代码?

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

根据我的经验,do-statement 是错误和混乱的根源。原因是它的主体总是在评估条件之前执行一次。然而,为了使身体正常工作,即使是第一次也必须保持与这种情况非常相似的东西。出乎我的意料,我发现无论是在程序第一次编写和测试时,还是在其前面的代码被修改后,这种情况都不会像预期的那样成立。 我也更喜欢“在我可以看到的前面”这个条件。因此,我倾向于避免使用 do 语句。 -Bjarne

避免 do/while 循环是包含在 C++ Core Guidelines 中的建议 ES.75, avoid do-statements

【问题讨论】:

  • @brandaemon 代码审查在 09 年 6 月还没有出现。你到底是什么意思?
  • @brandaemon 这是一个非常糟糕的建议......
  • @brandaemon StackOverflow 或 Programmers.SE。代码审查仅适用于完全工作的代码,并且必须包含代码。这个问题没有任何代码。这使它偏离了代码审查的主题。
  • @brandaemon 我会花一些时间阅读this meta post,它在这个主题上相当广泛。它提供了很多关于 Programmers.StackExchange 上的主题问题的见解。希望也能有所帮助!

标签: c++ c loops


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 先决条件?如果在一次执行主体后检查条件不是后置条件吗? :)
    • 这是一个我认为更具可读性的重写(并且嵌套更少) Handle handle = FindFirstFile(params);而(句柄!=错误){过程(参数);句柄 = FindNextFile(params); }
    • 是的,如果您不需要对每个边缘情况进行详细的错误处理。
    • 你不能在这里使用 while 循环来避免 if 吗?而((句柄= FindNextFile(参数))!=错误)){过程(参数);我的意思是,if 子句中的条件与循环中的条件完全相同。
    • @Dustin:也可以这样做(并且可能应该这样做)for(handle = FindFirstFile(params); handle != Error; handle = FindNextFile(params)) {}
    【解决方案3】:

    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-statement 是错误和混乱的根源。原因是它的主体总是在条件测试之前执行一次。然而,为了身体的正确运作,与最终条件类似的条件必须在第一次运行中保持。我发现这些条件不正确的次数比我预期的要多。当我从头开始编写有问题的程序然后对其进行测试时以及在更改代码之后都是这种情况。此外,我更喜欢“预先,我可以看到它”的条件。因此,我倾向于避免使用 do 语句。

    (注意:这是我对德语版的翻译。如果您碰巧拥有英文版,请随时编辑引用以匹配他的原始措辞。不幸的是,Addison-Wesley 讨厌 Google。)

    [1] B. Stroustrup: C++ 编程语言。第 3 版。艾迪生-韦斯利,雷丁,1997 年。

    【讨论】:

    • "gotos?豪华!当我还是个小伙子的时候,我们徒手抓住指令指针并将其拖到我们接下来要执行的代码上"
    • “拖?你们这些幸运的混蛋。我们不得不鞭打奴隶来推动我们的指令指针,这非常困难,因为我们是我们自己的奴隶。然后我们的妈妈会用破瓶子打我们睡觉。”
    • @Tobias - 感谢您找到报价。我把它放在原来的问题中(取自我的英文版)。
    • @Tobias 下面是一个示例,说明重写后的版本如何使完全清晰的代码更加混乱:假设您有一个循环计数器变量,用于计算循环执行的次数。在您重写的 while 循环中,这样的计数器应该在 loopBody 函数内还是在 while 循环内?答案是“视情况而定”,没有明显的答案。但在 do-while 循环中,这无关紧要。
    【解决方案4】:

    是的,我同意 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 循环,但这通常被视为更优雅的解决方案。

    【讨论】:

    • 这里是“while”版本:char c = -1; while (c '9') { printf(...);扫描(...); }
    • @Dustin,IMO 您的反例可读性较差,因为您使用了幻数(-1)。所以就“更好”的代码(当然这是主观的)而言,我认为@Rekreativc 的原始代码更好,因为它对人类 的眼睛来说更具可读性。
    • 我不确定我是否将其称为“神奇”数字,但无论如何,您都可以将其初始化为“char c = '0' - 1;”如果这更清楚。也许你真正反对的是'c' 有一个初始值这一事实?我同意这有点奇怪,但另一方面,“while”很好,因为它预先说明了继续条件,因此读者可以立即了解循环的生命周期。
    • 我的反对意见是肯定的,原则上,'c' 根本不应该有一个可观察的初始值。这就是为什么我更喜欢 do-while 版本的原因。 (尽管我认为您对现实世界的可读性有意见。)
    • @Dustin:重写的重要缺点是您需要从char 的域中保留一个元素(此处为-1)作为特殊标记。虽然这不会影响这里的逻辑,但通常这意味着您不再能够处理任意字符流,因为它可能包含值为 -1 的字符。
    【解决方案5】:

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

    因此,我对 Google Go 拥有opted to remove the do-while construct 感到非常失望。

    【讨论】:

    • 为什么宏需要这个?能举个例子吗?
    • 看看c2.com/cgi/wiki?TrivialDoWhileLoop>。预处理器宏不“处理”C 语法,因此需要像这样的巧妙技巧来防止宏插入不带大括号的 if 语句。
    • 网上有很多详细的原因,但我在谷歌搜索几分钟后找不到完整的链接。最简单的例子是它使多行宏表现为单行,这很重要,因为宏的用户可能会在不带括号的单行 for 循环中使用它,如果没有正确包装,这种用法会出现错误.
    • 我觉得 do while's inside #defines 很恶心。这是预处理器名声如此糟糕的原因之一。
    • 你应该澄清 Pod,你只是不喜欢它的美观,不喜欢额外的安全性,或者不同意它的必要性?帮助我理解。
    【解决方案6】:

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

    它可以像这样在 while 循环中伪造:

    while(true)
    {
    
        // .... code .....
    
        if(condition_satisfied) 
            break;
    }
    

    【讨论】:

    • 实际上,这并不等同于“do { ... code ... } while (condition)”,如果“... code ...”包含一个 'continue' 语句。 “继续”的规则是到底部做/一会儿。
    • 好点,但是,我说“fudge”,这意味着它有点错误
    【解决方案7】:

    我几乎从不使用它们,原因如下:

    即使循环检查后置条件,您仍需要在循环中检查此后置条件,以免处理后置条件。

    以示例伪代码为例:

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

    理论上听起来很简单,但在现实世界的情况下,它可能看起来像这样:

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

    额外的“如果”检查是不值得的 IMO。我发现很少有使用 do-while 而不是 while 循环更简洁的例子。 YMMV。

    【讨论】:

    • 这并不能真正回答问题。在您特别指出您可能已经处于不需要运行内部代码的状态的情况下,while() {...} 绝对是合适的选择 - while() {...} 正是为此而设计的您需要根据条件运行代码零次或多次的情况。但是,这并不意味着永远不会出现 do {...} while() 的情况,在这种情况下,您知道代码需要在检查条件之前至少运行一次(请参阅 @david-božjak 的选定答案)。
    【解决方案8】:

    一切都与可读性有关。
    更具可读性的代码可以减少代码维护的麻烦,以及更好的协作。
    到目前为止,在大多数情况下,其他考虑因素(例如优化)并不那么重要。
    我会详细说明,因为我在这里得到了评论:
    如果你有一个使用 do { ... } while() 的代码 sn-p A,并且它比它的 while() {...} 等效 B 更具可读性,那么我会投票给 一个。如果您更喜欢 B,因为您“预先”看到了循环条件,并且您认为它更具可读性(因此可维护等) - 然后继续,使用B
    我的观点是:使用对您的眼睛(以及您的同事)更具可读性的代码。当然,选择是主观的。

    【讨论】:

    • 没有人怀疑这一点,尤其是 OP。这绝对不能回答问题。
    【解决方案9】:

    响应未知(谷歌)对 Dan Olson 的回答提出的问题/评论:

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

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

    你知道没有do { ... } while(0) 会发生什么吗?它会一直执行 doAnother()。

    【讨论】:

    • 不过,你不需要做这件事。 #define M { one();二(); } 就足够了。
    • 但是如果(查询)M;否则 printf("你好");是语法错误。
    • 只有当你放一个 ;在 M 之后。否则它工作得很好。
    • 但更糟糕的是:这意味着用户在使用您的宏时必须记住,其中一些宏扩展为块而不是表达式。如果您有一个当前是 void 表达式,并且您需要将实现更改为需要两个语句的东西,那么现在它扩展为一个块,您必须更新所有调用代码。宏已经够糟糕的了:尽可能多地使用它们的痛苦应该在宏代码中,而不是在调用代码中。
    • @Markus:对于这个特定的例子,如果 do/while 在该语言中不可用,您可以将其替换为 #define M ((void) (doOneThing(), doAnother()))。
    【解决方案10】:

    在我们的编码约定中

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

    【讨论】:

    • 看到第一个示例中的错误了吗?也许 do-while 循环会让你避免这种情况。 :-)
    • 我讨厌用 do...while 循环阅读代码。就好像有人向您提供了您根本不关心的事情的详细信息,然后在 15 分钟的喋喋不休之后,他们告诉您为什么首先应该关心它。使代码可读。提前告诉我循环的用途,不要让我无缘无故滚动。
    • @Kieveli - 我的想法也是如此。我认为程序员习惯于在顶部设置条件,就像'for'和'if'一样。 do/while 真的很奇怪。
    • 大多数程序员似乎没有遇到中间条件的问题(if (...) break;),甚至经常与while(true) resp结合使用。 for(;;)。那么是什么让do ... while,你至少知道去哪里寻找条件变得更糟?
    • @celtschk 我以前的所有工作场所都认为if (bCondition) break; 是一个糟糕的设计,并鼓励使用适当的循环条件。当然有少数种情况是无法避免的,但大多数时候breakcontinue是懒惰编码的迹象。
    【解决方案11】:

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

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

    如果你看上面,TimW 的回复,它不言自明。第二个是 Handle,尤其是在我看来更混乱。

    【讨论】:

      【解决方案12】:

      阅读Structured Program Theorem。 do{} while() 始终可以重写为 while() do{}。序列、选择和迭代都是我们所需要的。

      由于循环体中包含的任何内容始终可以封装到例程中,因此必须使用 while() do{} 的肮脏程度永远不会比

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

      【讨论】:

      • 问题是当“LoopBody”变长(比如 10 行)并且您不想为它创建函数时会发生什么。重复这 10 行是愚蠢的。但在很多情况下,可以重新构造代码以避免重复这 10 行,但仍然使用“while”。
      • 当然,同样的代码两次维护对维护来说是很糟糕的。
      • 是的,当然有两个循环选项可以使代码更简单、更简洁。我的观点是(几十年来)已经证明两者都不是绝对必要的。
      • 另一个重写是:for (bool first=true; first || cond; first=false) { ... }。这不会重复代码,但会引入一个额外的变量。
      【解决方案13】:

      考虑这样的事情:

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

      '\0' 恰好是 0,但我希望你明白这一点。

      【讨论】:

      • 在什么方面{}while here 比 while{} 更干净?
      • 在 While{} 中,您必须在循环结束时添加代码。
      • 我不认为这是一个很好的例子。想象一下SumOfString("") 会发生什么。
      【解决方案14】:

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

      如果 while 只保留用于 while 循环并且 do em>/while 已更改为 do/untilrepeat/直到,我不认为循环(即当然很方便,而且编写一些循环的“正确”方法)会带来很多麻烦。

      I've ranted before about this in regards to JavaScript,它也继承了 C 的这个遗憾的选择。

      【讨论】:

      • 但是每个主要的编码风格都是一致的,他们总是把do-whilewhile写在}的同一行上。最后总是有一个分号。至少有一点点能力的 C 程序员在遇到以下行时会感到困惑或“绊倒”:} while(something);
      【解决方案15】:

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

      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 构造是一个非常有用的选项。

      【讨论】:

        【解决方案16】:

        好吧,也许这可以追溯到几步,但在

        的情况下
        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
        }
        

        但同样,可读性有所下降,更不用说在条件中使用赋值语句被认为是不好的做法,我只是想指出,有比分配“保留”更紧凑的方法向循环表明它是初始运行的值。

        【讨论】:

          【解决方案17】:

          这是我见过的最干净的替代方案。这是推荐给没有 do-while 循环的 Python 的习语。

          需要注意的是,您不能在 &lt;setup code&gt; 中使用 continue,因为它会跳过中断条件,但是显示 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;
          } 
          

          下一个示例是结构比 do-while 更具可读性的情况,因为条件保持在顶部附近,因为 //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!
          【解决方案18】:

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

          但令我惊讶的是,经过这么多答案,没有人考虑过为什么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,其中条件内的代码相对较慢(检查一些慢速硬件外围设备的响应)。

          【讨论】:

          • while (x == null || check(x)) . . . 中添加一个检查来解决这个问题
          • @D.B.Fred 但这仍然会带来更差的性能,因为您可能会在循环中的每一圈添加一个额外的分支。
          【解决方案19】:

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

          【讨论】:

            猜你喜欢
            • 2013-12-18
            • 2012-02-29
            • 1970-01-01
            • 1970-01-01
            • 2011-08-28
            • 1970-01-01
            • 1970-01-01
            • 2018-05-09
            • 2015-07-03
            相关资源
            最近更新 更多