235

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

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

什么是解决这个问题的快速而好的方法?

我在想异常并不便宜/应该只在真正异常的情况下抛出等。因此,从性能的角度来看,我认为这种解决方案不会很好。

我认为利用 .NET 中的新功能(匿名方法)来做一些非常基本的事情是不正确的。

4

25 回答 25

234

好吧,goto但这很丑陋,而且并不总是可能的。您还可以将循环放入方法(或匿名方法)中并用于return退出回到主代码。

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

与:

// 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 中,我们应该得到“本地函数”,这(语法待定等)意味着它应该像这样工作:

// 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");
于 2008-11-28T00:03:37.080 回答
101

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

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,不会神奇地跳转到外循环的下一次迭代——所以如果你有在内循环之外的代码,这种方法需要更多的检查。在这种情况下考虑其他解决方案。

这种方法适用于forandwhile循环,但不适用于foreach. 如果foreach您没有对隐藏枚举器的代码访问权限,因此您无法更改它(即使您IEnumerator没有某些“MoveToEnd”方法)。

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

于 2008-11-28T02:21:38.097 回答
44

此解决方案不适用于 C#

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

outer: while(fn1())
{
   while(fn2())
   {
     if(fn3()) continue outer;
     if(fn4()) break outer;
   }
}
于 2009-02-02T21:21:34.033 回答
31

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

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
}
于 2008-11-28T00:07:24.640 回答
23

不要引用我的话,但您可以按照 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 ) {}
于 2008-11-28T00:06:52.613 回答
16

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

于 2008-11-28T00:04:43.083 回答
14

在我看来,人们似乎很不喜欢一个goto声明,所以我觉得有必要稍微澄清一下。

我相信人们的“情绪”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;
    // ...
}

第二个翻译:forandbreak被翻译成更简单的等价物:

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和实际上是 的简写while,这部分是正确的。breakgoto

从 IL 到汇编程序

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

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

但是,我们现在有前向分支来实现我们的循环。您可能已经猜到了,这实际上是 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 优化期间发挥作用。

然而,分支往往会破坏那些很好的可预测模式,因此优化人员不太喜欢这种模式。Break,continue,goto——它们都打算打破这些可预测的模式——因此并不是真正的“好”。

在这一点上,您还应该意识到,一个简单的语句比foreach一堆到处乱跑的goto语句更容易预测。就(1)可读性和(2)从优化器的角度来看,它都是更好的解决方案。

另一件值得一提的是,它与优化编译器以将寄存器分配给变量(称为寄存器分配的过程)非常相关。您可能知道,您的 CPU 中只有有限数量的寄存器,它们是迄今为止硬件中最快的内存。在最内层循环中的代码中使用的变量更有可能被分配一个寄存器,而循环之外的变量则不太重要(因为此代码的命中率可能较低)。

求助,太复杂了……我该怎么办?

底线是您应该始终使用您可以使用的语言结构,这通常会(隐含地)为您的编译器构建可预测的模式。如果可能,尽量避免使用奇怪的分支(特别是:break、或 a中间什么都没有)。continuegotoreturn

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

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

现在我们来解决真正的问题。

想象一下,你有这样的东西:

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

在其他情况下,您还可以将该方法拆分为 2 个方法:

// 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环形。

换句话说,最好的情况是它最终会得到这样的结果:

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 循环中使用一个简单的条件。尽可能坚持使用您可以使用的高级语言结构。
  • 如果一切都失败了,而你只剩下gotoor bool more,那么更喜欢前者。
于 2016-03-02T18:47:33.063 回答
12

您要求快速、良好、不使用布尔值、不使用 goto 和 C# 的组合。你已经排除了所有可能的方式来做你想做的事。

最快最不难看的方法是使用 goto。

于 2008-11-28T00:47:14.107 回答
11

将因素纳入函数/方法并使用提前返回,或将循环重新排列为 while 子句。goto/exceptions/whatever 在这里肯定不合适。

def do_until_equal():
  foreach a:
    foreach b:
      if a==b: return
于 2008-11-28T00:10:42.293 回答
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;
            }
        }
    }
}
于 2013-12-14T01:04:32.767 回答
4

干净最短最可重用的方法是自调用匿名函数:

  • 没有转到
  • 无标签
  • 没有临时变量
  • 没有命名函数

使用匿名方法比最佳答案短一行。

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");
于 2020-04-12T18:03:11.850 回答
2

自从break几十年前我第一次看到 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.
于 2008-11-28T05:22:43.020 回答
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
    ...

}
于 2008-12-20T17:48:03.613 回答
1

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

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
        }
    }
}
于 2020-01-30T06:37:02.170 回答
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
  • 内循环后的代码会在break时执行
于 2020-01-30T07:25:31.160 回答
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;
}

...我希望这对那些喜欢我的人有所帮助是反 goto “fanboys” :)

于 2013-06-20T01:03:06.700 回答
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;
  }
}

于 2018-07-10T12:17:28.807 回答
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;
    }
}
于 2021-03-12T19:56:13.610 回答
0

人们经常忘记 for 循环的第二条语句本身就是中断条件,因此代码中不需要额外的 if。

像这样的工作:

bool run = true;
int finalx = 0;
int finaly = 0;
for (int x = 0; x < 100 && run; x++)
{
    finalx = x;
    for (int y = 0; y < 100 && run; y++)
    {
        finaly = y;
        if (x == 10 && y == 50) { run = false; }
    }
}
Console.WriteLine("x: " + finalx + " y: " + finaly);  // outputs 'x: 10 y: 50'
于 2022-01-13T15:09:03.867 回答
0

只需return在内部循环内使用,两个循环就会退出......

于 2022-02-28T22:22:26.313 回答
-3

抛出一个自定义异常,该异常退出外循环。

它适用于for, foreachorwhile任何类型的循环和任何使用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 
}
于 2014-09-10T14:03:03.457 回答
-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
            }
        }
于 2012-08-10T12:25:34.873 回答
-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;
               }

简单明了。:)

于 2015-03-19T11:03:49.330 回答
-6

你有没有看break关键字?哦

这只是伪代码,但你应该能够明白我的意思:

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

如果您考虑break成为类似 的函数break(),那么它的参数将是要跳出的循环数。由于我们在此处代码的第三个循环中,我们可以打破所有三个循环。

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

于 2013-12-14T01:26:29.673 回答
-11

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

于 2008-11-28T00:02:30.310 回答