如果我有一个嵌套在另一个中的for循环,我怎么能以最快的方式有效地从两个循环(内部和外部)中出来?
我不想使用布尔值,然后不得不说去另一个方法,而只是在外部循环后执行第一行代码.
这是一个快速而好的方式吗?
谢谢
我认为异常并不便宜/应该只是在一个真正特殊的条件下抛出等等.因此我认为从性能角度来看这个解决方案并不好.
我觉得利用.NET(anon方法)中的新功能做一些非常基本的事情是不对的.
因此,tvon(抱歉不能拼写完整的用户名!)有一个很好的解决方案.
Marc:很好地使用了anon方法,这也很棒但是因为我可以在一个不使用支持anon方法的.NET/C#版本的工作中,我也需要知道一种传统的方法.
嗯,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");
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
不会神奇地跳转到外循环的下一次迭代 - 所以如果你有内循环之外的代码,这种方法需要更多的检查.在这种情况下考虑其他解决方案
这种方法适用于for
和while
循环,但不起作用foreach
.如果foreach
你没有代码访问隐藏的枚举器,所以你不能改变它(即使你可能IEnumerator
没有一些"MoveToEnd"方法).
对内联评论作者的致谢:
Meta的i = INT_MAX - 1
建议/ ygoe的评论.
正确的jmbpiano通过内部循环后约代码备注blizpasta
for
foreach
IntMax
此解决方案不适用于C#
对于通过其他语言发现此问题的人,Javascript,Java和D允许标记的中断并继续:
outer: while(fn1()) { while(fn2()) { if(fn3()) continue outer; if(fn4()) break outer; } }
在外环中使用合适的防护装置.在中断之前将守卫设置在内环中.
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 }
不要引用我这个,但你可以按照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 ) {}
是否有可能将嵌套的for循环重构为私有方法?这样你就可以简单地"退出"退出循环.
在我看来,人们似乎很不喜欢这样的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代码):我们改变
break
并while
进入一个分支: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
和break
实际上短期手中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
,goto
或return
在没有任何中间).这里的好消息是,这些可预测的模式既易于阅读(适用于人类),也易于发现(适用于编译器).
其中一种模式称为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; } } } }