Выход из вложенного цикла
Если у меня есть цикл for, который вложен в другой, как я могу эффективно выйти из обоих циклов (внутренний и внешний) как можно быстрее?
Я не хочу использовать логическое значение, а затем должен сказать перейти к другому методу, а скорее просто выполнить первую строку кода после внешнего цикла.
Что это быстрый и хороший способ идти об этом?
спасибо
Я думал, что исключения не дешевы/должны быть только брошены в поистине исключительном состоянии и т. д. Поэтому я не думаю, что это решение было бы хорошо с точки зрения производительности.
Я не чувствую, что это правильно, чтобы воспользоваться новыми функциями в .NET (методы anon), чтобы сделать что-то, что является довольно фундаментальным.
из-за этого, tvon (извините не могу написать полное имя пользователя!) имеет хорошее решение.
Марк: хорошее использование методов anon, и это тоже здорово, но потому что я мог бы быть на работе, где мы этого не делаем используйте версию .NET / C#, которая поддерживает методы anon, мне тоже нужно знать традиционный подход.
9 ответов:
Ну
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");
Не знаю, если это работает в C#, но в C я часто делаю так:
for (int i = 0; i < 100; i++) { for (int j = 0; j < 100; j++) { if (exit_condition) { // cause the outer loop to break: i = INT_MAX; Console.WriteLine("Hi"); // break the inner loop break; } } }
это решение не относится к 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. Существуют и другие решения, в том числе флаг, который проверяется в каждой итерации обоих циклов. Наконец, вы можете использовать исключение как действительно тяжелое решение вашей проблемы.
Гото:
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 в отдельный метод? Таким образом, вы можете просто "вернуться" из метода, чтобы выйти из цикла.
фактор в функцию / метод и использовать раннее возвращение, или перестроить свои циклы в предложение while. goto / исключения / все, что здесь, конечно, не подходит.
def do_until_equal(): foreach a: foreach b: if a==b: return
вы попросили комбинацию быстрого, приятного, без использования логического значения, без использования goto и C#. Вы исключили все возможные способы делать то, что вы хотите.
самый быстрый и наименее уродливый способ-использовать goto.
мне кажется, что люди не любят a
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; // ... }
второй перевод: the
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;
в то время как компилятор делает эти вещи в один шаг, это дает вам представление о процессе. Код IL, который эволюционирует из программы C# - это дословный перевод последнего кода C#. Вы можете увидеть сами здесь:https://dotnetfiddle.net/QaiLRz (нажмите "посмотреть IL")
теперь, одна вещь, которую вы заметили здесь, заключается в том, что во время процесса код становится более сложным. Самый простой способ наблюдать это-тот факт, что нам нужно было все больше и больше кода, чтобы завершить то же самое. Вы также можете утверждать, что
foreach
,for
,while
иbreak
несколько на самом деле короткие руки дляgoto
, что отчасти верно.от IL к ассемблеру
интернет .Чистый JIT-компилятор-это компилятор ССА. Я не буду вдаваться во все детали формы 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-оптимизации.
тем не менее, ветви, как правило, нарушают эти приятные предсказуемые Шаблоны, что является чем-то оптимизатором, поэтому вид-Нелюбовь. Перерыв, продолжить, Гото-они все намерены чтобы сломать эти предсказуемые шаблоны-и поэтому не очень "приятно".
вы также должны понимать, что простой
foreach
более предсказуемо, чем кучаgoto
заявления, которые идут повсюду. С точки зрения (1) читаемости и (2) с точки зрения оптимизатора, это лучшее решение.еще одна вещь, о которой стоит упомянуть, заключается в том, что для оптимизации компиляторов очень важно назначать регистры переменным (процесс называется распределение регистров). Как вы, возможно, знаете, в вашем процессоре есть только конечное число регистров, и они на сегодняшний день являются самыми быстрыми частями памяти в вашем оборудовании. Переменные, используемые в коде, который находится во внутреннем цикле, скорее всего, получат назначенный регистр, в то время как переменные вне вашего цикла менее важны (потому что этот код, вероятно, меньше поражен).
помогите, слишком много сложностей... что же мне делать?
суть в том что вы всегда должны использовать языковые конструкции, которые у вас есть в вашем распоряжении, которые обычно (неявно) создают предсказуемые шаблоны для вашего компилятора. Старайтесь избегать странных ветвей, если это возможно (в частности:
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) { // ... } }
в других случаях вы также можете разделить метод на 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. // ... } // ...
я лично против такого подхода. Посмотрите еще раз на то, как код компилируется. Теперь подумайте, что это будет делать с этими хорошими, предсказуемыми моделями. Вы поняли картину?
хорошо, позвольте мне объяснить это. Что произойдет, так это:
- компилятор будет выписывать все как ветки.
- в качестве шага оптимизации компилятор будет выполнять поток данных