225

我已经用 Java 编程好几年了,但我最近才回到学校获得正式学位。我很惊讶地发现,在我的上一个作业中,我因为使用​​下面这样的循环而丢了分。

do{
     //get some input.
     //if the input meets my conditions, break;
     //Otherwise ask again.
} while(true)

现在对于我的测试,我只是在扫描一些控制台输入,但有人告诉我不鼓励这种循环,因为 usingbreak类似于goto,我们只是不这样做。

我完全理解goto及其 Java 表亲的陷阱break:label,并且我有很好的理由不使用它们。我也意识到一个更完整的程序会提供一些其他的逃避方式,比如结束程序,但这不是我的教授引用的原因,所以......

有什么问题do-while(true)

4

21 回答 21

222

我不会说这很糟糕——但同样,我通常至少会寻找替代方案。

在我写的第一件事的情况下,我几乎总是至少尝试将它重构为更清晰的东西。有时它无济于事(或者替代方法是有一个bool变量,它除了指示循环结束之外没有任何意义,不如break语句清晰),但至少值得尝试。

break作为一个比标志更容易使用的例子,请考虑:

while (true)
{
    doStuffNeededAtStartOfLoop();
    int input = getSomeInput();
    if (testCondition(input))
    {
        break;
    }
    actOnInput(input);
}

现在让我们强制它使用一个标志:

boolean running = true;
while (running)
{
    doStuffNeededAtStartOfLoop();
    int input = getSomeInput();
    if (testCondition(input))
    {
        running = false;
    }
    else
    {
        actOnInput(input);
    }
}

我认为后者阅读起来更复杂:它有一个额外的else块,actOnInput缩进更多,如果你想弄清楚 return 时会发生什么testConditiontrue你需要仔细查看块的其余部分以检查那里不是块之后的东西,else无论是否running设置为false

break语句更清楚地传达了意图,并让块的其余部分继续它需要做的事情,而不必担心早期的条件。

请注意,这与人们对方法中的多个 return 语句的论点完全相同。例如,如果我可以在前几行中计算出方法的结果(例如,因为某些输入为空、空或零),我发现直接返回该答案比使用变量来存储结果更清晰,然后是一整块其他代码,最后是一条return语句。

于 2011-07-27T19:55:11.807 回答
103

AFAIK 没什么,真的。老师们只是对 过敏goto,因为他们在某个地方听说它真的很糟糕。否则你只会写:

bool guard = true;
do
{
   getInput();
   if (something)
     guard = false;
} while (guard)

这几乎是一样的。

也许这更干净(因为所有循环信息都包含在块的顶部):

for (bool endLoop = false; !endLoop;)
{

}
于 2011-07-27T19:57:44.053 回答
39

Douglas Crockford 曾说过他希望JavaScript包含一个loop结构:

loop
{
  ...code...
}

而且我认为Javaloop也不会因为拥有一个结构而变得更糟。

循环本身并没有什么问题while(true),但教师倾向于阻止它们。从教学的角度来看,很容易让学生创建无限循环并且不理解为什么循环永远不会逃脱。

但他们很少提到的是,所有的循环机制都可以用while(true)循环来复制。

while( a() )
{
  fn();
}

是相同的

loop
{
  if ( !a() ) break;
  fn();
}

do
{
  fn();
} while( a() );

是相同的:

loop
{
  fn();
  if ( !a() ) break;
}

for ( a(); b(); c() )
{
  fn();
}

是相同的:

a();
loop
{
  if ( !b() ) break;
  fn();
  c();
}

只要您可以以一种可以工作的方式设置循环,您选择使用的结构并不重要。如果它恰好适合for循环,请使用for循环。

最后一部分:保持循环简单。如果每次迭代都需要执行很多功能,请将其放入函数中。在你让它工作后,你总是可以优化它。

于 2011-07-28T01:45:12.923 回答
19

早在 1967 年,Edgar Dijkstra 在一本贸易杂志上写了一篇关于为什么应该从高级语言中去除 goto 以提高代码质量的文章。一个称为“结构化编程”的完整编程范式由此产生,但当然不是每个人都同意 goto 自动意味着糟糕的代码。

结构化编程的关键本质上是代码的结构应该决定它的流程,而不是尽可能有 goto 或中断或继续决定流程。类似地,在该范例中也不鼓励对循环或函数具有多个入口和出口点。

显然这不是唯一的编程范式,但它通常可以很容易地应用于其他范式,如面向对象编程(ala Java)。

您的老师可能已经被教导过,并且正在尝试通过确保我们的代码是结构化的并遵循结构化编程的隐含规则来告诉您的班级我们最好避免“意大利面条式代码”。

虽然使用 break 的实现本身并没有什么“错误”,但有些人认为在 while() 条件中明确指定循环条件的代码更容易阅读,并消除了一些过于棘手的可能性。使用 while(true) 条件肯定有一些陷阱,这些条件似乎在新手程序员的代码中经常出现,例如意外创建无限循环的风险,或者使代码难以阅读或不必要地混淆。

具有讽刺意味的是,异常处理是一个肯定会出现偏离结构化编程的领域,并且随着您进一步使用 Java 编程而出现这种情况。

也有可能你的导师希望你展示你使用特定循环结构或语法的能力,而你编写的代码在功能上是等效的,但你可能没有展示您应该在那节课中学习的特定技能。

于 2011-07-28T21:09:03.707 回答
14

读取输入的常用 Java 约定是:

import java.io.*;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String strLine;

while ((strLine = br.readLine()) != null) {
  // do something with the line
}

通常用于读取输入的 C++ 约定是:

#include <iostream>
#include <string>
std::string data;
while(std::readline(std::cin, data)) {
  // do something with the line
}

在 C 中,它是

#include <stdio.h>
char* buffer = NULL;
size_t buffer_size;
size_t size_read;
while( (size_read = getline(&buffer, &buffer_size, stdin)) != -1 ){
  // do something with the line
}
free(buffer);

或者如果你确信你知道文件中最长的文本行有多长,你可以这样做

#include <stdio.h>
char buffer[BUF_SIZE];
while (fgets(buffer, BUF_SIZE, stdin)) {
  //do something with the line
}

如果您正在测试您的用户是否输入了quit命令,则可以轻松扩展这 3 个循环结构中的任何一个。我会用 Java 为你做:

import java.io.*;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;

while ((line = br.readLine()) != null  && !line.equals("quit") ) {
  // do something with the line
}

因此,虽然在某些情况下breakorgoto是合理的,但如果您所做的只是逐行读取文件或控制台,那么您不需要while (true)循环来完成它——您的编程语言已经为您提供了使用适当的习语将输入命令用作循环条件。

于 2011-07-28T00:42:23.547 回答
12

这并不是一件可怕的事情,但是您在编码时需要考虑其他开发人员。即使在学校。

您的开发人员应该能够在循环声明处看到循环的退出子句。你没有那样做。您将 exit 子句隐藏在循环的中间,为其他出现并试图理解您的代码的人做更多的工作。这与避免诸如“中断”之类的事情的原因相同。

话虽如此,您仍然会在现实世界中的大量代码中看到类似的内容。

于 2011-07-27T19:58:47.080 回答
12

这是你的枪,你的子弹和你的脚......

这很糟糕,因为你在自找麻烦。不会是您或此页面上的任何其他海报有短/简单 while 循环的示例。

麻烦将在未来某个非常随机的时间开始。它可能是由另一个程序员引起的。可能是安装软件的人。它可能是最终用户。

为什么?我必须找出为什么一个 700K LOC 的应用程序会逐渐开始消耗 100% 的 CPU 时间,直到每个 CPU 都饱和。这是一个惊人的 while (true) 循环。它又大又讨厌,但归结为:

x = read_value_from_database()
while (true) 
 if (x == 1)
  ...
  break;
 else if (x ==2)
  ...
  break;
and lots more else if conditions
}

没有最终的 else 分支。如果该值与 if 条件不匹配,则循环继续运行直到时间结束。

当然,程序员责怪最终用户没有选择程序员期望的值。(然后我消除了代码中的所有 while(true) 实例。)

恕我直言,使用 while(true) 之类的结构并不是好的防御性编程。它会回来困扰你。

(但我确实记得如果我们没有评论每一行,即使是 i++,教授也会降级;)

于 2011-07-29T02:06:47.653 回答
6

从某种意义上说,结构化编程结构优于(有些非结构化的)break 和 continue 语句,这是不好的。相比之下,根据这一原则,它们更倾向于“goto”。

我总是建议使您的代码尽可能结构化......尽管,正如 Jon Skeet 指出的那样,不要让它比这更结构化!

于 2011-07-27T20:00:51.263 回答
5

根据我的经验,在大多数情况下,循环具有“主要”条件才能继续。这是应该写入 while() 运算符本身的条件。所有其他可能破坏循环的条件都是次要的,不是那么重要等。它们可以写成附加if() {break}语句。

while(true)经常令人困惑并且可读性较差。

我认为这些规则不能涵盖 100% 的情况,但可能只涵盖 98% 的情况。

于 2011-07-27T19:59:18.720 回答
3

您可能只使用一个布尔标志来指示何时结束 while 循环。Break并且go to是软件难以维护的原因 - 软件危机(tm) - 应该避免,也可以很容易地避免。

你是否务实,这是个问题。务实的编码员可能只是在这种简单的情况下使用 break。

但是养成不使用它们的习惯是好的,否则你可能会在不合适的情况下使用它们,比如在复杂的嵌套循环中,使用break.

于 2011-07-27T19:56:58.620 回答
3

虽然不一定是为什么不使用的答案while (true),但我一直发现这部漫画和随附作者的陈述是关于为什么要使用 while 而不是 do-while 的简洁解释。

关于您的问题:与

while(true) {
   do_stuff();
   if(exit_time) {
      break;
   }
}

...如果您知道自己在做什么并确保exit_time在某些时候评估为true.

老师不鼓励您使用while(true),因为除非您完全知道自己在做什么,否则这是犯严重错误的简单方法。

于 2011-07-28T12:24:02.333 回答
3

我认为是的,这很糟糕......或者至少对于许多开发人员来说。这是不考虑循环条件的开发人员的症状。因此容易出错。

于 2011-08-04T02:31:14.413 回答
2

while(true)with语句没有大问题break,但是有些人可能认为它会稍微降低代码的可读性。尝试给变量起有意义的名字,在适当的地方评估表达式。

对于您的示例,执行以下操作似乎更清楚:

do {
   input = get_input();
   valid = check_input_validity(input);    
} while(! valid)

如果 do while 循环变得很长,则尤其如此——您确切地知道检查的位置是否发生了额外的迭代。所有变量/函数在抽象级别都有适当的名称。该while(true)声明确实告诉您处理不在您想象的地方。

也许您希望第二次通过循环获得不同的输出。就像是

input = get_input();
while(input_is_not_valid(input)) {
    disp_msg_invalid_input();
    input = get_input();
}

对我来说似乎更具可读性

do {
    input = get_input();
    if (input_is_valid(input)) {
        break;
    }
    disp_msg_invalid_input();
} while(true);

同样,通过一个简单的示例,两者都非常易读;但是如果循环变得非常大或嵌套很深(这意味着您可能应该已经重构),第一种样式可能会更清晰一些。

于 2011-07-28T04:36:22.607 回答
2

也许我运气不好。或者,也许我只是缺乏经验。但是每次我回想起处理insidewhile(true)break,都可以改进将Extract Method应用于while-block的代码,它保留了while(true)but(巧合?)将所有breaks 转换为returns。

根据我的经验while(true),没有休息(即返回或投掷)非常舒适且易于理解。


  void handleInput() {
      while (true) {
          final Input input = getSomeInput();
          if (input == null) {
              throw new BadInputException("can't handle null input");
          }
          if (input.isPoisonPill()) {
              return;
          }
          doSomething(input);
      }
  }
于 2011-07-28T13:28:15.067 回答
1

这更像是一种美学,更容易阅读代码,您可以明确知道为什么循环会在循环声明中停止。

于 2011-07-27T19:57:09.250 回答
1

我在很多函数中使用了类似的东西,但逻辑相反。

DWORD dwError = ERROR_SUCCESS;

do
{
    if ( (dwError = SomeFunction()) != ERROR_SUCCESS )
    {
         /* handle error */
         continue;
    }

    if ( (dwError = SomeOtherFunction()) != ERROR_SUCCESS )
    {
         /* handle error */
         continue;
    }
}
while ( 0 );

if ( dwError != ERROR_SUCCESS )
{
    /* resource cleanup */
}
于 2011-07-28T16:44:25.237 回答
1

我想说,通常它不被认为是一个好主意的原因是您没有充分利用该构造。另外,我倾向于认为很多编程教师不喜欢他们的学生带着“行李”进来。我的意思是我认为他们喜欢成为影响学生编程风格的主要因素。所以也许这只是教练的一个小问题。

于 2011-07-29T20:56:15.463 回答
1

对我来说,问题在于可读性。

条件为真的 while 语句不会告诉您有关循环的任何信息。它使理解它的工作变得更加困难。

从这两个片段中更容易理解什么?

do {
  // Imagine a nice chunk of code here
} while(true);

do {
  // Imagine a nice chunk of code here
} while(price < priceAllowedForDiscount);
于 2011-08-03T03:19:25.120 回答
0

我猜对你的老师使用休息就像打破树枝来获取果实,使用一些其他技巧(弯曲树枝)以便你得到果实并且树枝还活着。:)

于 2011-07-29T07:37:56.967 回答
0

1) 没有任何问题do -while(true)

2) 你的老师错了。

国家安全局!!:

3) 大多数老师是老师而不是程序员。

于 2011-07-29T20:19:43.700 回答
0

如果您的循环在后台线程上运行可能会很糟糕,因此当您通过终止 UI 线程关闭应用程序时,该段代码将继续执行。正如其他人已经说过的那样,您应该始终使用某种支票来提供取消的方式。

于 2011-08-02T15:08:44.720 回答