当前位置:  开发笔记 > 编程语言 > 正文

打破嵌套循环

如何解决《打破嵌套循环》经验,为你挑选了10个好方法。

如果我有一个嵌套在另一个中的for循环,我怎么能以最快的方式有效地从两个循环(内部和外部)中出来?

我不想使用布尔值,然后不得不说去另一个方法,而只是在外部循环后执行第一行代码.

这是一个快速而好的方式吗?

谢谢


我认为异常并不便宜/应该只是在一个真正特殊的条件下抛出等等.因此我认为从性能角度来看这个解决方案并不好.

我觉得利用.NET(anon方法)中的新功能做一些非常基本的事情是不对的.

因此,tvon(抱歉不能拼写完整的用户名!)有一个很好的解决方案.

Marc:很好地使用了anon方法,这也很棒但是因为我可以在一个不使用支持anon方法的.NET/C#版本的工作中,我也需要知道一种传统的方法.



1> Marc Gravell..:

嗯,goto但这很难看,并不总是可能的.您还可以将循环放入方法(或anon-method)并用于return退回到主代码.

    // goto
    for (int i = 0; i < 100; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            goto Foo; // yeuck!
        }
    }
Foo:
    Console.WriteLine("Hi");

VS:

// anon-method
Action work = delegate
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits anon-method
        }
    }
};
work(); // execute anon-method
Console.WriteLine("Hi");

请注意,在C#7中我们应该得到"本地函数",其中(语法tbd等)意味着它应该工作如下:

// local function (declared **inside** another method)
void Work()
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits local function
        }
    }
};
Work(); // execute local function
Console.WriteLine("Hi");


GOTO很好很好.这就是它存在于C#(以及几乎所有)语言中的原因.我觉得"不喜欢GOTO"很酷.顺便说一下,这就是答案:)
在这种情况下,我不认为使用goto比正常使用像break这样的东西更糟糕(毕竟它们都只是标签的无条件分支,它只是带有break的标签是隐含的).
转到本身并不难看.什么是丑陋的滥用goto导致意大利面条代码.使用goto打破嵌套循环是完美的.此外,请注意,从结构编程的角度来看,所有突破,继续和返回都不比goto好 - 基本上它们是相同的,只是在更好的包装中.这就是为什么纯结构语言(比如原始的Pascal)缺少三个.
有时goto不如替代品那么邪恶
@ el.pescado是完全正确的:很多程序员都误导人们相信"goto"本身就是有害的*,而它只是用来做某些事情的**正确的**工具,并且像许多乐器一样可能被滥用.这种对"goto"的宗教厌恶坦率地说是非常愚蠢的,*绝对是*非科学的.
C#有一个"goto"似乎很奇怪,但它没有像其他语言那样标记为"break"或"continue".鉴于这种语言限制,我认为`goto`正是这个问题的正确解决方案.
@BeowulfOF - break只会突破内循环,而不是内循环和外循环.
goto的唯一问题是,一旦你把那个标签放在那里.错误语句有可能在goto和循环结束之间插入,使得中断代码也跳过该语句.例如,Java通过仅允许标签标记循环的末尾来防止这种情况.
呃,他提到的另一个选择是什么,这是一个简单的回复声明?我们现在不喜欢功能吗?自从我们用大量代码编写所有内容以来,已经有几十年了.今天,我们将这些东西称为"函数",这意味着您不需要使用goto.试试吧.
我认为这两个是最好的解决方案.我认为goto的使用在这里是合理的.而且它也适用于很多其他平台(在我的情况下是delphi,这就是我查看的原因)
功能还可以,但Dijkstra还说多次退货都是意大利面。
@SinanILYAS死记硬背的规则,例如“永不使用goto”,是唯一的“菜鸟”。将默认值设置为“ avoid goto”是可以的-我可以尊重-但是:如果您不了解操作的内容和原因,那么*任何*都是不正确的指导。如果您了解要避免的内容以及原因,那么即使“确实”是在99%的时间内,您也可以了解该指导何时适用和不适用。对于另一个示例,“所有struct都应为readonly struct或-很少(有原因)-ref struct`”是菜鸟的重要指南,但现实变得更加有趣比起那个来说。

2> Nils Pipenbr..:

C#自适应方法经常用于C - 外部循环变量的设置值在循环条件之外(即对于循环使用int变量INT_MAX -1通常是不错的选择):

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 100; j++)
    {
        if (exit_condition)
        {
            // cause the outer loop to break:
            // use i = INT_MAX - 1; otherwise i++ == INT_MIN < 100 and loop will continue 
            i = int.MaxValue - 1;
            Console.WriteLine("Hi");
            // break the inner loop
            break;
        }
    }
    // if you have code in outer loop it will execute after break from inner loop    
}

正如代码中的注释所说,break不会神奇地跳转到外循环的下一次迭代 - 所以如果你有内循环之外的代码,这种方法需要更多的检查.在这种情况下考虑其他解决方案

这种方法适用于forwhile循环,但不起作用foreach.如果foreach你没有代码访问隐藏的枚举器,所以你不能改变它(即使你可能IEnumerator没有一些"MoveToEnd"方法).

对内联评论作者的致谢: Meta的
i = INT_MAX - 1建议/ ygoe的评论. 正确的jmbpiano通过内部循环后约代码备注blizpasta
forforeach
IntMax


你应该使用i = INT_MAX - 1; 否则i ++ == INT_MIN <100并且循环将继续
关于foreach的任何想法?
@ktutnik它不能与foreach一起使用,因为您将无法访问隐藏的枚举器.IEnumerator也没有一些"MoveToEnd"方法.

3> BCS..:

此解决方案不适用于C#

对于通过其他语言发现此问题的人,Javascript,Java和D允许标记的中断并继续:

outer: while(fn1())
{
   while(fn2())
   {
     if(fn3()) continue outer;
     if(fn4()) break outer;
   }
}


令人遗憾的是,这不能用c#完成,它会在很多时候产生更清晰的代码.
我实际上兴奋了一秒钟,直到我意识到这不是c#.:(

4> tvanfosson..:

在外环中使用合适的防护装置.在中断之前将守卫设置在内环中.

bool exitedInner = false;

for (int i = 0; i < N && !exitedInner; ++i) {

    .... some outer loop stuff

    for (int j = 0; j < M; ++j) {

        if (sometest) {
            exitedInner = true;
            break;
        }
    }
    if (!exitedInner) {
       ... more outer loop stuff
    }
}

或者更好的是,将内部循环抽象为方法,并在返回false时退出外部循环.

for (int i = 0; i < N; ++i) {

    .... some outer loop stuff

    if (!doInner(i, N, M)) {
       break;
    }

    ... more outer loop stuff
}


除了OP说"我不想使用布尔值".

5> David Rodríg..:

不要引用我这个,但你可以按照MSDN中的建议使用goto.还有其他解决方案,包括在两个循环的每次迭代中检查的标志.最后,您可以使用异常作为解决问题的重要解决方案.

去:

for ( int i = 0; i < 10; ++i ) {
   for ( int j = 0; j < 10; ++j ) {
      // code
      if ( break_condition ) goto End;
      // more code
   }
}
End: ;

条件:

bool exit = false;
for ( int i = 0; i < 10 && !exit; ++i ) {
   for ( int j = 0; j < 10 && !exit; ++j ) {
      // code
      if ( break_condition ) {
         exit = true;
         break; // or continue
      }
      // more code
   }
}

例外:

try {
    for ( int i = 0; i < 10 && !exit; ++i ) {
       for ( int j = 0; j < 10 && !exit; ++j ) {
          // code
          if ( break_condition ) {
             throw new Exception()
          }
          // more code
       }
    }
catch ( Exception e ) {}


:)对,这是一个简单的解决方案,但你必须将所有必需的本地数据作为参数传递给方法...这是goto可能是适当解决方案的少数几个地方之一

6> NoizWaves..:

是否有可能将嵌套的for循环重构为私有方法?这样你就可以简单地"退出"退出循环.



7> atlaste..:

在我看来,人们似乎很不喜欢这样的goto陈述,所以我觉得有必要将这一点理顺.

我相信人们所拥有的"情感" goto最终归结为对代码的理解以及对可能的性能影响的(误解).在回答这个问题之前,我首先会详细介绍它的编译方法.

众所周知,C#被编译为IL,然后使用SSA编译器将其编译为汇编程序.我将对这一切是如何工作给出一些见解,然后尝试回答问题本身.

从C#到IL

首先,我们需要一段C#代码.让我们开始简单:

foreach (var item in array)
{
    // ... 
    break;
    // ...
}

我会一步一步地让你对发动机罩下的情况有所了解.

第一个翻译:从foreach等到for循环(注意:我在这里使用数组,因为我不想深入了解IDisposable的细节 - 在这种情况下我还必须使用IEnumerable):

for (int i=0; i

第二次翻译:for并将break其翻译成更容易的等价物:

int i=0;
while (i < array.Length)
{
    var item = array[i];
    // ...
    break;
    // ...
    ++i;
}

第三次翻译(这相当于IL代码):我们改变breakwhile进入一个分支:

    int i=0; // for initialization

startLoop:
    if (i >= array.Length) // for condition
    {
        goto exitLoop;
    }
    var item = array[i];
    // ...
    goto exitLoop; // break
    // ...
    ++i;           // for post-expression
    goto startLoop; 

虽然编译器只需一步即可完成这些操作,但它可让您深入了解该过程.从C#程序演变而来的IL代码是最后一个C#代码的字面翻译.你可以在这里看到:https://dotnetfiddle.net/QaiLRz(点击'查看IL')

现在,您在此处观察到的一件事是,在此过程中,代码变得更加复杂.观察这个的最简单方法是我们需要越来越多的代码来完成相同的事情.您可能还认为foreach,for,whilebreak实际上短期手中goto,这部分是真实的.

从IL到汇编

.NET JIT编译器是一个SSA编译器.我不会在这里讨论SSA表单的所有细节以及如何创建优化编译器,它太多了,但可以对将要发生的事情有一个基本的了解.为了更深入地理解,最好开始阅读优化编译器(我喜欢这本书的简要介绍:http://ssabook.gforge.inria.fr/latest/book.pdf)和LLVM(llvm.org) .

每个优化编译器都依赖于代码很容易并且遵循可预测模式的事实.在FOR循环的情况下,我们使用图论来分析分支,然后在我们的分支中优化诸如cycli之类的东西(例如向后分支).

但是,我们现在有前向分支来实现我们的循环.您可能已经猜到,这实际上是JIT要修复的第一步,如下所示:

    int i=0; // for initialization

    if (i >= array.Length) // for condition
    {
        goto endOfLoop;
    }

startLoop:
    var item = array[i];
    // ...
    goto endOfLoop; // break
    // ...
    ++i;           // for post-expression

    if (i >= array.Length) // for condition
    {
        goto startLoop;
    }

endOfLoop:
    // ...

如您所见,我们现在有一个向后分支,这是我们的小循环.这里唯一令人讨厌的事情是由于我们的break声明我们最终得到的分支.在某些情况下,我们可以以相同的方式移动它,但在其他情况下,它会留在那里.

那么为什么编译器会这样做呢?好吧,如果我们可以展开循环,我们可以对它进行矢量化.我们甚至可以证明只有常量被添加,这意味着我们的整个循环可能会消失得无影无踪.总结一下:通过使模式可预测(通过使分支可预测),我们可以证明某些条件在我们的循环中成立,这意味着我们可以在JIT优化期间做出魔术.

然而,分支往往打破那些好的可预测模式,这是优化者因此有点 - 不喜欢.打破,继续,转到 - 他们都打算打破这些可预测的模式 - 因此并不是真的'好'.

在这一点上你也应该意识到一个简单foreach的东西比一堆goto遍布各处的语句更容易预测.就(1)可读性而言,(2)从优化器的角度来看,它是更好的解决方案.

另一件值得一提的是,它与优化编译器将寄存器分配给变量(称为寄存器分配的过程)非常相关.您可能知道,CPU中只有有限数量的寄存器,它们是硬件中最快的内存块.在最内层循环中的代码中使用的变量更有可能获得分配的寄存器,而循环外部的变量则不那么重要(因为此代码可能会受到较少的影响).

帮助,太复杂......我该怎么办?

最重要的是,您应该始终使用您拥有的语言结构,这通常会(隐含地)为您的编译器构建可预测的模式.尽量避免陌生的分支(如果可能具体是:break,continue,gotoreturn在没有任何中间).

这里的好消息是,这些可预测的模式既易于阅读(适用于人类),也易于发现(适用于编译器).

其中一种模式称为SESE,代表单输入单一退出.

现在我们得到了真正的问题.

想象一下,你有这样的事情:

// a is a variable.

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...

     if (i*j > a) 
     {
        // break everything
     }
  }
}

使这个成为可预测模式的最简单方法是简单地完全消除if:

int i, j;
for (i=0; i<100 && i*j <= a; ++i) 
{
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
}

在其他情况下,您还可以将方法拆分为两种方法:

// Outer loop in method 1:

for (i=0; i<100 && processInner(i); ++i) 
{
}

private bool processInner(int i)
{
  int j;
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
  return i*j<=a;
}

临时变量?好,坏或难看?

您甚至可能决定从循环内返回一个布尔值(但我个人更喜欢SESE表单,因为这是编译器看到它的方式,我认为它更清晰).

有些人认为使用临时变量更清晰,并建议这样的解决方案:

bool more = true;
for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j) 
  {
     // ...
     if (i*j > a) { more = false; break; } // yuck.
     // ...
  }
  if (!more) { break; } // yuck.
  // ...
}
// ...

我个人反对这种做法.再看一下代码是如何编译的.现在想想这对这些漂亮,可预测的模式会有什么影响.得到图片?

好吧,让我拼出来吧.会发生什么是:

编译器会将所有内容写成分支.

作为优化步骤,编译器将进行数据流分析,以尝试删除more仅恰好在控制流中使用的奇怪变量.

如果成功,变量more将从程序中消除,并且仅保留分支.这些分支将被优化,因此您将只从内循环中获得一个分支.

如果不成功,则变量more肯定在最内层循环中使用,因此如果编译器不会将其优化掉,则很有可能将其分配给寄存器(这会占用宝贵的寄存器内存).

因此,总结一下:编译器中的优化器会遇到很多麻烦,弄清楚more它只用于控制流,并且在最好的情况下,它会将它转换为外部的一个分支.环.

换句话说,最好的情况是它最终会得到相当于:

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...
     if (i*j > a) { goto exitLoop; } // perhaps add a comment
     // ...
  }
  // ...
}
exitLoop:

// ...

我个人对此的看法非常简单:如果这是我们一直以来的想法,那么让编译器和可读性更容易让世界变得更容易,并立即写下来.

TL;博士:

底线:

如果可能,请在for循环中使用简单条件.尽可能坚持您所掌握的高级语言结构.

如果一切失败,留给你要么goto或者bool more,更喜欢前者.



8> Dustin Getz..:

将因子转换为函数/方法并使用早期返回,或将循环重新排列为while子句.转到/例外/这里肯定不合适的.

def do_until_equal():
  foreach a:
    foreach b:
      if a==b: return



9> Windows prog..:

你要求快速,好,不使用布尔值,不使用goto和C#的组合.你已经排除了做你想做的所有可能的方法.

最快捷,最简单的方法是使用goto.



10> 小智..:

有时很好地将代码抽象到它自己的函数中,而不是使用早期的返回 - 尽管早期的返回是邪恶的:)

public void GetIndexOf(Transform transform, out int outX, out int outY)
{
    outX = -1;
    outY = -1;

    for (int x = 0; x < Columns.Length; x++)
    {
        var column = Columns[x];

        for (int y = 0; y < column.Transforms.Length; y++)
        {
            if(column.Transforms[y] == transform)
            {
                outX = x;
                outY = y;

                return;
            }
        }
    }
}

推荐阅读
yzh148448
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有