2015-05-18 2 views
3

(Сейчас я в основном использую C#. Идеи на других языках приветствуются, но, пожалуйста, переведите их на C#, если сможете, и явным.)Есть ли «умный» способ вырваться из вложенных циклов?

Что-то, с чем я сталкиваюсь снова и снова, является вложенным циклом , просматривая какой-либо 2D-массив, чтобы найти элемент (обычно некоторый объект), который затем нужно манипулировать. Поэтому, конечно, как только вы обнаружите этот объект, вы должны вырваться из обоих циклов, чтобы вы не продолжали искать что-то, что уже было найдено (особенно в вложенных циклах, которые могут пересекать экспоненциально огромные массивы).

Следующий код в настоящее время мой предпочтительный способ сделать это:

Obj O = null; 
bool KeepLooping = true; 

for (int j = 0; j < height && KeepLooping; j++) 
{ 
    for (int i = 0; i < width; i++) 
    { 
     if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value) 
     { 
      O = ObjArray[i, j]; // you found it, now remember it 
      KeepLooping = false; // clear the flag so the outer loop will break too 
      break; 
     } 
    } 
} 

Благодаря Эрик Funkenbusch, он становится гораздо более элегантным, если мы делаем это:

Obj O = null; 
for (int j = 0; j < height && O == null; j++) // much, much better idea to check O for null in the outer loop 
{ 
    for (int i = 0; i < width; i++) 
    { 
     if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value) 
     { 
      O = ObjArray[i, j]; // you found it, now remember it 
      break; 
     } 
    } 
} 

Больше нет необходимости в том, что pesky extra boolean!

Тем не менее поиск альтернативных или лучших решений продолжается. На протяжении многих лет я пытался много других способов, но найти их не так уж велика по тем или иным причинам: один к значению выше height

  1. Set j (итератор внешнего контура), который вызовет его сломать автоматически. Не идеально, потому что иногда вы хотите запомнить значения i и j, где вы его нашли.
  2. Используйте foreach на 2D-массиве. Не идеально, так как foreachне позволит вам манипулировать коллекцией (удалите или добавьте к ней, что очень часто является причиной того, что я ищу объект в любом случае).
  3. Просто поместите 2 петли в функцию, которая ничего не делает, кроме как найти и вернуть O. return эффективно разрывает обе петли. Много раз это нормально, но не всегда. Я делаю это для очень общих поисков, но есть также довольно много «групповых поисков», где я хочу коллективизировать прохождение. В этих случаях я нахожу 2 или более объектов (иногда в пределах одного и того же 2D-массива), помню их и только потом вырывался из обоих циклов.
  4. Использовать goto? (Whoa, может ли это быть единственным законным использованием goto? Это удивительно читаемо, чем флаг KeepLooping, особенно если у нас есть три или более петли.) Не идеально, потому что коллеги будут кричать кровавое убийство. И в C#, будет ли правильная очистка мусора после goto?
  5. Выбросить собственное исключение? idk, я никогда не пробовал, но он выглядит менее читаемым, чем мой предпочтительный способ.
  6. Как только вы нашли нужный объект, сделайте все свои манипуляции с объектом внутри внутреннего цикла, а затем return;Это может стать беспорядочным быстро. Иногда манипуляция объектом включает в себя свои собственные петли.

Существует также очень умный седьмой путь, благодаря User_PWY:

int size = width*height; // save this so you dont have to keep remultiplying it every iteration 
for (int i = 0; i < size; i++) 
{ 
    int x = i % width; // ingenious method here 
    int y = i/width; // ingenious method here 

    O = ObjArray[x, y]; 

    if (O != null) 
     break; // woohoo! 
} 

Это эффективно уплотняет 2D массива в одну for цикла для итерации. Однако некоторые критики отметили, что мода и операторы деления довольно медленны по сравнению с только i ++ или j ++, поэтому он может быть медленнее (помните, что мы имеем дело с 2D-массивами того, кто знает, какой размер). Как я прокомментировал, должен быть способ получить разделение и остаток в одной операции, потому что я уверен, что ассемблерный код x86 имеет коды операций DIV, которые хранят частное и остальное в отдельных регистрах, все в одной инструкции DIV. Но как это сделать/использовать в C#, idk.

Было бы неплохо, если бы C# разрешили вам называть циклы, такие как L1 и L2, а затем сделать что-то вроде L1.break(); независимо от того, какой цикл вы внутри. Увы ... это невозможно сделать на этом языке. (Может ли быть секретный способ сделать это с помощью макросов?) Есть ли когда-нибудь C# 6.0 с этой функцией?

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

Хорошо, расскажите, пожалуйста, ваш предпочтительный способ, особенно если это что-то не перечисленное здесь.

+0

'goto' в C# по причине, так что если вы чувствуете, что это законное использование случае Гото и имеют показатели производительности, чтобы доказать это, ваши коллеги не должны жаловаться. – zzzzBov

+1

'goto' не меняется, как работает сборщик мусора. – MarcinJuraszek

+0

@zzzzBov все в порядке, но сложно сказать коллегам, что они должны и не должны жаловаться, если вы не босс (я не):/ – DrZ214

ответ

2
for (int i = 0; i < width*height; i++) 
{ 
    int x=i%width 
     ,y=i/width; 
    //dostuff 
} 

Мне нравится этот способ доступа к массиву 2d.

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

комментарий 2)
о макроре. Я не смог найти код, но сумел коротко произвести его.

#define FOR2DARRAY(WIDTH,HEIGHT) 
    \for (int i = 0, x = 0,y = 0; i < (WIDTH)*(HEIGHT); i++, x=i%(WIDTH),y=i/(HEIGHT)) 
+0

Nice. Кажется смутно знакомым, я, возможно, использовал его один или два раза в хаосе с массивами для какой-то другой цели. Теперь я действительно хочу пойти на охоту через свои архивы ... но я уверен, что никогда не видел и не слышал об этом для разрыва вложенных циклов, поэтому я отметил это. Это работает для любой ширины х высоты? или он должен быть квадратом? – DrZ214

+0

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

+0

iirc, оператор мод не очень дорог. Разумеется, Отдел. Что касается умножения в заголовке цикла, мы можем просто сделать что-то вроде 'int size = width * height', поэтому мы делаем умножение только один раз, затем запомним его и используем' size' в заголовке цикла вместо более буквального 'witdth * height'. – DrZ214

1

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

Goto возможен для этого случая (и это по существу только случай использования), если вы не можете найти лучший подход. Флаг установки с читаемым именем, вероятно, вызовет меньше вопросов/криков.

ли не

  • исключения использования для потока управления
  • переменные изменения цикла. Выяснить, что происходит, очень сложно в этом случае. Я бы пообещал, что большинство людей примет goto как лучший подход.
+0

Ломать из вложенности в LINQ довольно сложно без исключений. Можете ли вы показать пример? – Gabe

+0

@Gabe один вариант - 'SelectMany' для сглаживания вложенных циклов и' TakeWhile'/'SkipWhile' /' First'/'Where' для фильтрации/обрезания цикла short –

0

не очень сильно отличается, но я предпочитаю использовать булеву флаг таким образом:

Obj O = null; 
bool found = false; 

for (int j = 0; j < height; j++) 
{ 
    for (int i = 0; i < width; i++) 
    { 
     if (ObjArray[i, j] != null && ObjArray[i, j].property = search_value) 
     { 
      O = ObjArray[i, j]; // you found it, now remember it 
      found = true; // clear the flag so the outer loop will break too 
      break; 
     } 
    } 
    if (found) break; 
} 

if (!found) 
{ 
    // The loop finished with no values found 
} 
else 
{ 
    // Do stuff here with the value found 
} 
1

goto является прекрасно подходит для этого, Microsoft даже recommends it:

goto передаёт программный контроль непосредственно в обозначение .

Общее использование goto заключается в передаче управления на конкретный ярлык коммутатора или метку по умолчанию в инструкции коммутатора.

Операция goto также полезна для выхода из глубоко вложенных петель.

Что касается вашего вопроса о разрушении объекта после того, как вы вышли из сферы для контекстного объекта сборщика мусор должен пометить его для уничтожения, но я не могу точно сказать, является ли поведение таким же, если вы используйте, например, директиву using вокруг вашей петли for и goto вне ее, в отличие от обычной области.

1

Попробуйте это:

Obj O = ObjArray.Cast<Obj>().FirstOrDefault(obj => obj != null && obj.property == search_value); 

Это преобразует 2D массив Obj в IEnumerable<Obj> и дает первый тот, который соответствует вашим условиям. Если ни один из них не соответствует, Obj O настроен на null.

Проверьте это здесь: https://dotnetfiddle.net/dQTJbU

1

Может быть, я пропустил его в списке опций, но почему вы не можете реорганизовать поиск в метод, который возвращает свой объект?

private object FindObject(....) 
{ 
    for (int j = 0; j < height && KeepLooping; j++) 
    { 
     for (int i = 0; i < width; i++) 
     { 
      if (ObjArray[i, j] != null && ObjArray[i, j].property = search_value) 
      { 
       return ObjArray[i, j]; // you found it, now remember it 
      } 
     } 
    } 
    return null; 
} 
+0

Да, это решение номер 3, который я перечислил. Я использую его примерно в половине случаев, но это не всегда адекватно. См. Перечисленный пункт 3, почему. – DrZ214

+0

@ DrZ214 - Хорошо, но я не понимаю, почему, если вы хотите больше объектов, просто заставьте функцию вернуть список. – Ray

1

Существует много способов сделать это. В вашем примере, я хотел бы сделать это следующим образом:

Obj O = null; 

for (int j = 0; j < height && O == null; j++) 
{ 
    for (int i = 0; i < width; i++) 
    { 
     if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value) 
     { 
      O = ObjArray[i, j]; // you found it, now remember it 
      break; 
     } 
    } 
} 

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

Вы могли бы на самом деле даже упростить это больше:

Obj O = null; 

for (int j = 0; j < height && O == null; j++) 
{ 
    for (int i = 0; i < width && O == null; i++) 
    { 
     if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value) 
      O = ObjArray[i, j]; // you found it, now remember it 
    } 
} 

Теперь перерыв даже не требуется.

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

FYI, проблема с изменением коллекции в foreach на самом деле не проблема, если вы делаете это правильно. Например:

// Assumes ObjArray is [,] and not [][] 
foreach (int item in ObjArray.ToList()) // makes a copy to iterate over 
{ 
    if (item != null && item.property == search_value) { 
     ObjArray[0,0] = item; // I can now modify this for no reason 
     break; 
    } 
} 
+0

Эй, спасибо за этот еще более короткий метод, используя '&& O == null' вместо' KeepLooping'! Гораздо более элегантный, без дополнительного булева. Но я бы не сделал этого, как в вашем втором примере. 'Break; 'сохраняет нам несколько дополнительных инструкций, немедленно разбивая их, вместо того, чтобы возвращаться и делать это сравнение. Кроме того, размещение '&& O == null' во внутреннем цикле также означает, что мы делаем это сравнение на каждой итерации, когда это действительно необходимо только во внешнем цикле. – DrZ214

+0

@ DrZ214 - Вы не дали понять, что вы заботитесь о нескольких дополнительных циклах процессора. Тем не менее, операции сравнения дешевы, если они являются внутренними типами. –

+0

Хорошо, я вернулся и дал понять. Мне нравится ваше первое решение и добавлено в мой OP. – DrZ214

Смежные вопросы