打破嵌套循环

时间:2008-11-27 23:58:44

标签: c# for-loop nested-loops

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

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

快速而好的方法是什么?

由于


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

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

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

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

24 个答案:

答案 0 :(得分:188)

嗯,goto,但这很丑陋,并不总是可能的。您还可以将循环放入方法(或anon方法)并使用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");

答案 1 :(得分:93)

经常在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的建议 for / foreach ygoe发表评论。
jmbpiano正确的IntMax 通过blizpasta对内循环后的代码进行评论

答案 2 :(得分:39)

此解决方案不适用于C#

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

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

答案 3 :(得分:26)

在外环中使用合适的防护装置。在你休息之前在内环中设置守卫。

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
}

答案 4 :(得分:19)

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

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

答案 5 :(得分:14)

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

答案 6 :(得分:11)

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

我相信&#39;情感&#39;人们大约goto最终归结为对可能的性能影响的代码和(误解)的理解。在回答这个问题之前,我首先会详细介绍一下它是如何编写的。

众所周知,C#被编译为IL,然后使用SSA编译器将其编译为汇编程序。我将对这一切的工作原理给出一些见解,然后尝试回答这个问题。

从C#到IL

首先我们需要一段C#代码。让我们开始吧:

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

我将逐步完成这项工作,让您了解幕后发生的事情。

第一次翻译:从foreach到等效的for循环(注意:我在这里使用数组,因为我不想进入IDisposable的详细信息 - 在这种情况下,我还必须使用IEnumerable):

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

第二次翻译:{{1​​}}和for被翻译成更容易的等价物:

break

第三次翻译(这相当于IL代码):我们将int i=0; while (i < array.Length) { var item = array[i]; // ... break; // ... ++i; } break更改为分支:

while

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

现在,您在此处观察到的一件事是,在此过程中,代码变得更加复杂。观察这个的最简单方法是我们需要越来越多的代码来完成相同的事情。您可能还认为 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; foreachforwhile实际上是break的短手,这部分属实。

从IL到汇编程序

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

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

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

goto

正如你所看到的,我们现在有一个向后的分支,这是我们的小循环。在这里,唯一令人讨厌的事情是由于 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: // ... 声明我们最终得到的分支。在某些情况下,我们可以以同样的方式移动它,但在其他情况下,它会留在那里。

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

然而,分支倾向于打破那些好的可预测模式,这是优化者因此有点 - 不喜欢。打破,继续,转到 - 他们都打算打破这些可预测的模式 - 因此并不是真的很好......#/ p>

此时你还应该意识到,一个简单的break比一堆foreach语句更可预测。就(1)可读性和(2)从优化器的角度而言,它是更好的解决方案。

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

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

最重要的是,您应该始终使用您可以使用的语言结构,这通常会(隐含地)为您的编译器构建可预测的模式。尽可能避免使用奇怪的分支(特别是gotobreakcontinuegoto

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

其中一种模式称为SESE,代表Single Entry Single Exit。

现在我们回到真正的问题。

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

return

使这种模式成为可预测模式的最简单方法是简单地完全消除// 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)
  {
     // ...
  }
}

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

你甚至可能决定从循环中返回一个布尔值(但我个人更喜欢SESE表单,因为编译器将会看到它并且我认为阅读更清晰)。

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

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

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

是的,让我拼出来吧。会发生什么:

  • 编译器会将所有内容写成分支。
  • 作为优化步骤,编译器将进行数据流分析,以尝试删除仅恰好在控制流中使用的奇怪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仅用于控制流,而在最佳情况下< / strong>会将其转换为外部for循环之外的单个分支。

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

more

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

<强> TL; DR:

底线:

  • 如果可能,请在for循环中使用简单条件。尽可能坚持您所掌握的高级语言结构。
  • 如果所有内容都失败了,而您选择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: // ... goto,则更喜欢前者。

答案 7 :(得分:11)

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

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

答案 8 :(得分:8)

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

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

答案 9 :(得分:5)

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

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

答案 10 :(得分:2)

根据您的具体情况,您可以这样做,但前提是您在内循环后没有执行代码。

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 100; j++)
    {
        i = 100;
        break;
    }
}

这不是很好,但根据你的问题,它可能是最简单的解决方案。

答案 11 :(得分:2)

我见过很多使用“break”但没有使用“continue”的例子。

它仍然需要在内循环中使用某种标志:

while( some_condition )
{
    // outer loop stuff
    ...

    bool get_out = false;
    for(...)
    {
        // inner loop stuff
        ...

        get_out = true;
        break;
    }

    if( get_out )
    {
        some_condition=false;
        continue;
    }

    // more out loop stuff
    ...

}

答案 12 :(得分:1)

自从我几十年前第一次在C中看到break以来,这个问题让我感到烦恼。我希望一些语言增强功能可以延伸到可以解决的问题:

break; // our trusty friend, breaks out of current looping construct.
break 2; // breaks out of the current and it's parent looping construct.
break 3; // breaks out of 3 looping constructs.
break all; // totally decimates any looping constructs in force.

答案 13 :(得分:1)

另一个选择是自行调用的匿名函数。 没有goto,没有标签,没有新变量,没有使用新函数名,并且比顶部的anonymous-method example短了一行。

// self-invoked-anonymous-function
new Action(() =>
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits self invoked lambda expression
        }
    }
})();
Console.WriteLine("Hi");

答案 14 :(得分:1)

可以在循环中使用自定义条件来破坏循环,从而使代码清晰。

    static void Main(string[] args)
    {
        bool isBreak = false;
        for (int i = 0; ConditionLoop(isBreak, i, 500); i++)
        {
            Console.WriteLine($"External loop iteration {i}");
            for (int j = 0; ConditionLoop(isBreak, j, 500); j++)
            {
                Console.WriteLine($"Inner loop iteration {j}");

                // This code is only to produce the break.
                if (j > 3)
                {
                    isBreak = true;
                }                  
            }

            Console.WriteLine("The code after the inner loop will be executed when breaks");
        }

        Console.ReadKey();
    }

    private static bool ConditionLoop(bool isBreak, int i, int maxIterations) => i < maxIterations && !isBreak;   

使用此代码,我们获得以下输出:

  • 外部循环迭代0
  • 内循环迭代0
  • 内循环迭代1
  • 内循环迭代2
  • 内循环迭代3
  • 内循环迭代4
  • 内循环之后的代码将在中断时执行

答案 15 :(得分:0)

结束双循环的最简单方法是直接结束第一个循环

string TestStr = "The frog jumped over the hill";
char[] KillChar = {'w', 'l'};

for(int i = 0; i < TestStr.Length; i++)
{
    for(int E = 0; E < KillChar.Length; E++)
    {
        if(KillChar[E] == TestStr[i])
        {
            i = TestStr.Length; //Ends First Loop
            break; //Ends Second Loop
        }
    }
}

答案 16 :(得分:0)

这就是我做到的方式。仍然是一种解决方法。

foreach (var substring in substrings) {
  //To be used to break from 1st loop.
  int breaker=1;
  foreach (char c in substring) {
    if (char.IsLetter(c)) {
      Console.WriteLine(line.IndexOf(c));
      \\setting condition to break from 1st loop.
      breaker=9;
      break;
    }
  }
  if (breaker==9) {
    break;
  }
}

答案 17 :(得分:0)

此处未提及的另一个既干净又不依赖于较新的 .NET 功能的选项是将双循环合并为产品上的单循环。然后在循环内部,可以使用简单的数学计算计数器的值:

int n; //set to max of first loop
int m; //set to max of second loop

for (int k = 0; k < n * m; k++)
{
    //calculate the values of i and j as if there was a double loop
    int i = k / m;
    int j = k % m;
    
    if(exitCondition)
    {
        break;
    }
}

答案 18 :(得分:0)

我记得在我的学生时代,据说在数学上证明你可以在没有goto的情况下在代码中做任何事情(即没有goto是唯一答案的情况)。所以,我从不使用goto(只是我个人的偏好,不是暗示我是对还是错)

无论如何,为了打破嵌套循环我会做这样的事情:

var isDone = false;
for (var x in collectionX) {
    for (var y in collectionY) {
        for (var z in collectionZ) {
            if (conditionMet) {
                // some code
                isDone = true;
            }
            if (isDone)
                break;
        }
        if (isDone) 
            break;
    }
    if (isDone)
        break;
}

...我希望对那些喜欢我的人有帮助的是反对“粉丝”:)

答案 19 :(得分:-3)

抛出一个外循环的自定义异常。

适用于forforeachwhile或任何类型的循环以及使用try catch exception阻止的任何语言

try 
{
   foreach (object o in list)
   {
      foreach (object another in otherList)
      {
         // ... some stuff here
         if (condition)
         {
            throw new CustomExcpetion();
         }
      }
   }
}
catch (CustomException)
{
   // log 
}

答案 20 :(得分:-4)

正如我所见,你接受了这个人给你转到goto语句的答案,在现代编程和专家意见中goto是一个杀手,我们把它称为编程中的杀手,这有某些原因,我不会讨论在这一点,但你的问题的解决方案很简单,你可以在这种场景中使用布尔标志,就像我将在我的例子中演示它:

            for (; j < 10; j++)
            {
                //solution
                bool breakme = false;
                for (int k = 1; k < 10; k++)
                {
                   //place the condition where you want to stop it
                    if ()
                    {
                        breakme = true;
                        break;
                    }
                }

                if(breakme)
                    break;
               }
简单明了。 :)

答案 21 :(得分:-4)

         bool breakInnerLoop=false
        for(int i=0;i<=10;i++)
        {
          for(int J=0;i<=10;i++)
          {
              if(i<=j)
                {
                    breakInnerLoop=true;
                    break;
                }
          }
            if(breakInnerLoop)
            {
            continue
            }
        }

答案 22 :(得分:-6)

您是否看过break关键字? O.o

这只是伪代码,但您应该能够看到我的意思:

<?php
for(...) {
    while(...) {
        foreach(...) {
            break 3;
        }
    }
}

如果您认为breakbreak()之类的函数,那么它的参数就是要突破的循环数。由于我们在这里的代码的第三个循环中,我们可以突破所有三个。

手动:http://php.net/break

答案 23 :(得分:-8)

我认为除非你想做“布尔事物”,否则唯一的解决办法就是扔掉。你显然不应该这样做..!