Выход из вложенного цикла

Если у меня есть цикл 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.


мне кажется, что люди не любят goto заявление много, поэтому я почувствовал необходимость немного исправить это.

я считаю, что "эмоции" люди имеют о goto в конечном итоге сводится к пониманию кода и (неправильных представлений) о возможных последствиях для производительности. Прежде чем ответить на вопрос, я сначала рассмотрю некоторые детали о том, как он составлен.

как мы все знаем, C# компилируется в IL, который затем компилируется в ассемблер с использованием компилятора SSA. Я дам немного информации о том, как все это работает, а затем попытаюсь ответить на сам вопрос.

от C# до IL

Сначала нам нужен кусок кода на C#. Давайте начнем просто:

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

я буду делать это шаг за шагом, чтобы дать вам хорошее представление о том, что происходит под капотом.

первый перевод: с foreach в эквиваленте for loop (Примечание: я использую массив здесь, потому что я не хотите узнать подробности IDisposable - в этом случае мне также придется использовать IEnumerable):

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

второй перевод: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) с точки зрения оптимизатора это лучшее решение.

еще одна вещь, заслуживающая упоминания, заключается в том, что для оптимизации компиляторов очень важно назначить регистры переменным (процесс называется распределение регистров). Как вы знаете, в вашем процессоре есть только конечное количество регистров, и они являются самыми быстрыми частями памяти в вашем оборудовании. Переменные, используемые в коде, который находится во внутреннем цикле most, с большей вероятностью получат назначенный регистр, в то время как переменные вне вашего цикла менее важны (потому что этот код, вероятно, попадает меньше).

помощь, слишком много сложности... что мне делать?

нижняя линия что вы всегда должны использовать языковые конструкции, которые есть в вашем распоряжении, которые обычно (неявно) создают предсказуемые шаблоны для вашего компилятора. Старайтесь избегать странных ветвей, если это возможно (в частности: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.
  // ...
}
// ...

лично я против такого подхода. Посмотрите еще раз на то, как компилируется код. Теперь подумайте, что это будет делать с этими хорошими, предсказуемыми шаблонами. Поняли картину?

хорошо, позвольте мне объяснить это. Что произойдет, так это:

  • компилятор запишет все как ветви.
  • в качестве шага оптимизации компилятор будет выполнять поток данных