8

我经常听说break在 Java 中使用 s 被认为是不好的做法,但是在阅读了 Stack Overflow 上的一些线程之后,我发现并非如此。许多人说在某些情况下这是可以接受的。

在这种情况下,我对什么是/不是不好的做法有点困惑。

对于Project Euler:问题 7,我构建了以下代码。挑战是找到第 10001 个素数。

int index = 2, count = 1, numPrime = 1;

while (true) {
    index++;

    if (isPrime(index)) {
        count = index;
        numPrime++;
    }

    if (numPrime >= 10001)
        break;
}

System.out.println(count);

这会返回正确的答案(在 21 毫秒内),但我是否忽略了一个严重的警告?100% 有可能创建一个没有中断的 while 循环,但我发现这更容易理解。

是我使用break;不良做法的方式吗?我知道总有办法使用它,但这里真的有那么可怕吗?

非常感谢

贾斯蒂安

编辑

这是我的 isPrime() 代码。我还不如在我做的时候优化它。

public static boolean isPrime(long num) {  
    if (num == 2)
        return true;

    if (num % 2 == 0 || num <= 0)
        return false;

    for (long i = 3; i * i <= num; i += 2)
        if (num % i == 0)
            return false;

    return true;
}
4

9 回答 9

22

我不确定休息通常是不好的做法,但我认为这是一个。

这有点愚蠢,因为它是不必要的。你while (true) ... break的完全等同于:

while (numPrime < 10001) {
   ...
}

但不那么直观。

坚持使用规范的方式来表示事物意味着您的读者的计算开销更少,使您的代码更易于理解,因此更易于维护并最终更健壮。

编辑(回应评论):你是对的,总有一种方法可以使用一个,但规则(如果你想要一个)相对简单:写任何最易读的东西。在您发布的情况下,这不是最易读的,因为我给出的替代方案是表示这一点的规范方式。在某些情况下,例如循环遍历集合直到找到匹配的特定候选者,使用break可能表示此模式的最合适的方式。

试图提出关于何时使用break以及何时提取变量并改用条件句的硬性规则将是困难的(我会争辩说,徒劳的)。通常很容易判断什么是最简单的选择,但并非总是如此。这是经验真正重要的例子之一——你自己阅读和编写的好/坏代码越多,你个人的好坏例子库就越大,你就越容易说出“最好的”是什么"* 表示给定概念的方式是。

*当然是主观的!

于 2010-07-30T16:11:20.963 回答
15

在这种情况下,在我看来,仅仅改变while条件会更容易:

while (numPrime < 10001) {

这通常是while(true)循环结束时的情况

if (condition)
{
    break;
}

...虽然您需要检查循环主体中的其他任何内容是否执行continue.

或者,您可以稍微重组它:

int current = 1;
for (int i = 0; i < 10001; i++)
{
    current++;
    while (!isPrime(current))
    {
        current++;
    }
}

那么current最后会有答案。

for当您尝试执行特定次数的操作时,我通常更喜欢循环而不是 while 循环。在这种情况下,“某事”是“找到下一个素数”。

我觉得编程中有各种各样的教条走得太远了——包括“一个方法的出口点”和“不要使用中断”。尽可能编写可读的代码。如果您查看一些代码并觉得其中发生的事情并不明显,请尝试找出其他构造它的方法。有时这是改变循环的情况;有时它是提取方法;有时它会颠倒一些逻辑(首先处理否定分支,可能提前退出,然后处理正常情况)。

于 2010-07-30T16:12:01.757 回答
2

如果你可以不休息就离开,那就去做吧。

 while (numPrime < 10001) ...
于 2010-07-30T16:12:34.483 回答
2

这就是 do/while 的发明目的:

do {
//...
} while(numPrime < 10001);

while(true)是我发现不好的做法,这当然会导致break.

于 2010-07-30T16:14:14.500 回答
2

有几个条件使用休息是有意义的。一种是当您需要执行 N.5 个循环时——即,您将执行该循环若干次,但最后一次您将始终在循环体中间的某个位置执行。在这种情况下,您可以避免使用中断,但这样做通常会混淆代码或导致重复。例如:

while (true) {
    first part;
    if (finished) 
       break;
    second part;
}

可以变成类似的东西:

first part;
while (!finished) {
    second part;
    first part;
}

或者:

while (!finished) {
    first part;
    if (!finished)
        second part;
}

这些都不一定是重大改进。中断有意义的另一种情况是简单地处理诸如错误之类的事情。例如,如果您已被传递 N 个文件进行处理,那么如果其中一个文件无法打开,则退出循环可能是有意义的。尽管如此,当它完全合理时,显然最好在循环条件中明确说明退出循环的条件。

于 2010-07-30T16:25:45.707 回答
2

与之前所说的没有太大不同,但从可读性、透明逻辑的角度来看,我会推荐

long i,primes=0;
for (i=2;primes<10001;i++) {
    if (isPrime(i))
        primes++;
}

我是答案。

于 2010-07-30T19:45:50.517 回答
0

如果只有一个条件可以退出循环,并且在检查条件之前不需要运行大量代码,并且在检查条件后不需要运行大量代码,将条件放入循环中。

我确实认为“do{}while(1);”肯定有用途。环形。其中:

  1. 在循环继续的情况下,主要条件需要在检查之前和之后运行足够的代码,以确保将代码放在条件本身中充其量是尴尬的。
  2. 循环有多个退出条件,最符合逻辑地位于循环顶部或底部的退出条件需要执行特殊代码,而其他退出条件不应执行。

有些人喜欢使用标志来区分退出条件。我倾向于将这些标志视为一种努力,以避免实际上最能体现程序动作的编码结构。

如果一个人不太关心速度,可以避免使用任何形式的goto、过早退出return,或者就此而言使用超过一段时间- 并且有一个简单的条件。只需将一个人的整个程序编写为状态机:

无效do_whatever(无效)
{
  int current_state=1;
  做
  {
    下一个状态 = 0;
    如果(当前状态 == 1)
    {
      do_some_stuff();
      下一个状态 = 2;
    }
    如果(当前状态 == 2)
    {
      do_some_more_stuff();
      下一个状态 = 3;
    }
    ...
    当前状态 = 下一个状态;
  } 而(当前状态);
}

有时这种编码很有用(特别是如果“while”可以从 do_whatever() 例程中拉出到一个循环中,该循环“同时”运行几个类似编码的例程)。而且从来没有必要使用“goto”之类的东西。但是为了可读性,结构化编程结构要好得多。

在我看来,使用标志退出循环,然后根据退出原因选择要执行的几段代码之一就是用非结构化代码替换结构化代码。如果在一个循环中我写

  如果(索引> = numItems)
  {
    创建新项目(索引);
    休息;
  }

立即(并且在本地)清楚的是,我创建新项目的原因是因为我的索引超出了项目的数量,并且我不需要重复测试条件。相反,如果我循环直到找到某些东西或用完项目,那么我将不得不在循环之后冗余地测试一个条件,或者在每个循环迭代中添加一个标志测试。

于 2010-07-30T16:58:27.323 回答
0

正如其他人所指出的,您给出的示例很糟糕,因为您可以轻松地将测试移至 WHILE。

我使用中断的情况是当我有一个循环时,我必须在我知道这是最后一次之前进行一些处理。例如:

while (true)
{
  String inLine=inStream.readLine();
  if (inLine.startsWith("End"))
    break;
  ... process line ...
}

这个例子只是稍微做作:我经常遇到问题,我从文件或其他源读取一些数据,以某种方式解析这些数据,然后才知道我现在已经到了我感兴趣的处理的尽头.

当然,您可以编写一个函数来读取该行并进行必要的解析,例如:

while (!endOfInterestingData(inStream))
{
  ... process ...
}

但是你可能会遇到问题,函数和循环体都需要访问读取的数据,并且函数必须返回一个布尔值来控制循环处理,所以它不能返回数据,所以唯一的方法是循环体可用的数据是让函数将其放入某个可相互访问的字段中,然后该字段会掩盖循环从何处获取其数据。

于 2010-07-30T16:59:22.927 回答
0

这个解决方案非常快:

#include <stdio.h>

unsigned long isqrt(unsigned long n) {
    // http://snippets.dzone.com/posts/show/2715
    unsigned long a;
    for (a = 0; n >= (2*a)+1; n -= (2*a++) + 1);
    return a;
}

void nPrimes(const long N, long *const primes)
{
    unsigned long n, count, i, root;

    primes[0] = 2;
    count = 1;

    for (n = 3; count < N; n+=2) {

        root = isqrt(n);

        for (i = 0; primes[i] <= root; i++) {
            if ((n % primes[i]) == 0) goto notPrime;
        }
/*      printf("Prime #%lu is: %lu\n", count+1, n);*/
        primes[count++] = n;

        notPrime: ;
    }
}

int main (int argc, char **argv)
{
    long N;

    if (argc > 1) {
        N = atoi(argv[1]);
    } else {
        N = 10001;
    }

    long primes[N];

    nPrimes(N, primes);

    printf("Prime #%lu is: %lu\n", N, primes[N-1]);
}

笔记:

  • isqrt(n) 是地板(sqrt(n))。这避免了我们不需要的浮点运算。来自http://snippets.dzone.com/posts/show/2715的算法。
  • nPrimes 保存一个素数列表。这使您可以仅测试主要除数,从而减少了绝大多数昂贵的 mod 操作。
  • 即使是素数也只能测试到 iqrt(n)。
  • 那是一个丑陋的 goto,但 C 没有命名 continue,标志变量将完全浪费时间。
  • 素数数组用 2 初始化,它只检查 3 中的奇数。消除 3 的倍数同样可行,但并不复杂。
  • 可以通过保留一个素数平方表来消除 isqrt 调用。这几乎可以肯定是矫枉过正,但如果你愿意,你可以。
  • 在我不是新的机器上,这在眨眼间就运行了。
于 2010-07-31T18:03:51.507 回答