Почему я должен использовать foreach вместо for (int i = 0; i <length; i ++) в циклах? - PullRequest
45 голосов
/ 08 декабря 2010

Кажется, что классный способ создания циклов в C # и Java - это использовать foreach вместо стиля C для циклов.

Есть ли причина, по которой я должен предпочесть этот стиль стилю C?

Меня особенно интересуют эти два случая, но, пожалуйста, рассмотрите столько случаев, сколько вам нужно, чтобы объяснить свои моменты.

  • Я хочу выполнить операцию для каждого элемента в списке.
  • Я ищу предмет в списке и хочу выйти, когда этот предмет найден.

Ответы [ 18 ]

78 голосов
/ 08 декабря 2010

Представьте, что вы шеф-повар ресторана, и вы все готовите огромный омлет для фуршета. Вы даете коробку с дюжиной яиц каждому из двух сотрудников кухни и в буквальном смысле говорите им, что они ломаются.

Первый устанавливает миску, открывает ящик, захватывает каждое яйцо по очереди - слева направо по верхнему ряду, затем по нижнему ряду - разбивая его по краям чаши и затем опорожняя в чашу. , В конце концов у него кончаются яйца. Хорошо выполненная работа.

Второй устанавливает чашу, открывает ящик, а затем бросается прочь, чтобы получить лист бумаги и ручку. Он пишет цифры от 0 до 11 рядом с отсеками для яиц и номер 0 на бумаге. Он смотрит на число на бумаге, находит отделение с надписью 0, вынимает яйцо и трескает его в миску. Он снова смотрит на 0 на бумаге, думает, что «0 + 1 = 1», вычеркивает 0 и пишет 1 на бумаге. Он хватает яйцо из отделения 1 и разбивает его. И так далее, пока число 12 не окажется на бумаге, и он не узнает (не глядя!), Что яиц больше нет. Хорошо выполненная работа.

Вы бы подумали, что второй парень немного запутался в голове, верно?

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


Кроме того, структуры типа связанного списка не могут быть эффективно обработаны путем увеличения счетчика и индексации в: «индексирование» означает начало отсчета с начала. В C мы можем обработать связанный список, который мы сделали сами, используя указатель для цикла «counter» и разыменовывая его. Мы можем сделать это в современном C ++ (и в некоторой степени в C # и Java), используя «итераторы», но это все еще страдает от проблемы косвенности.


Наконец, некоторые языки имеют достаточно высокий уровень, и идея написания цикла для «выполнения операции над каждым элементом в списке» или «поиска элемента в списке» ужасна (точно так же, как шеф-повар не должен рассказывать первому кухонному сотруднику, как убедиться, что все яйца взломаны). Предусмотрены функции, которые устанавливают эту структуру цикла, и вы сообщаете им - с помощью функции более высокого порядка или, возможно, значения сравнения, в случае поиска - что делать в цикле. (На самом деле, вы можете делать это в C ++, хотя интерфейсы несколько неуклюжи.)

71 голосов
/ 08 декабря 2010

Я могу придумать две основные причины:

1) Абстрагируется от базового типа контейнера. Это означает, например, что вам не нужно изменять код, который перебирает все элементы в контейнере, когда вы меняете контейнер - вы указываете goal of "делать это для каждого элемент в контейнере ", а не означает .

2) Исключает возможность возникновения ошибок по одному.

С точки зрения выполнения операции с каждым элементом в списке, интуитивно просто сказать:

for(Item item: lst)
{
  op(item);
}

Он отлично выражает намерение читателя, в отличие от ручной работы с итераторами. То же самое для поиска предметов.

37 голосов
/ 08 декабря 2010
  • foreach проще и удобочитаемее

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

  • Не все коллекции поддерживают произвольный доступ; единственный способ итерации HashSet<T> или Dictionary<TKey, TValue>.KeysCollection - это foreach.

  • foreach позволяет перебирать коллекцию, возвращаемую методом, без дополнительной временной переменной:

    foreach(var thingy in SomeMethodCall(arguments)) { ... }
    
19 голосов
/ 08 декабря 2010

Одним из преимуществ для меня является то, что менее легко допускать ошибки, такие как

    for(int i = 0; i < maxi; i++) {
        for(int j = 0; j < maxj; i++) {
...
        }
    }

ОБНОВЛЕНИЕ: Это один из способов, с помощью которых происходит ошибка.Я делаю сумму

int sum = 0;
for(int i = 0; i < maxi; i++) {
    sum += a[i];
}

, а затем решаю собрать ее больше.Поэтому я обертываю цикл в другой.

int total = 0;
for(int i = 0; i < maxi; i++) {
   int sum = 0;
    for(int i = 0; i < maxi; i++) {
        sum += a[i];
    }
    total += sum;
}

Компиляция не удалась, конечно, поэтому мы вручную редактируем

int total = 0;
for(int i = 0; i < maxi; i++) {
   int sum = 0;
    for(int j = 0; j < maxj; i++) {
        sum += a[i];
    }
    total += sum;
}

В коде теперь как минимум две ошибки (и больше, еслимы запутали макси и макси), которые будут обнаружены только во время выполнения.И если вы не пишете тесты ... и это редкий кусок кода - это укусит кого-то еще - плохо.

Вот почему это хорошая идея, чтобы извлечь внутренний цикл в метод:

int total = 0;
for(int i = 0; i < maxi; i++) {
    total += totalTime(maxj);
}

private int totalTime(int maxi) {
   int sum = 0;
    for(int i = 0; i < maxi; i++) {
        sum += a[i];
    }
    return sum;
}

и он более читабельный.

16 голосов
/ 08 декабря 2010

foreach будет работать идентично for во всех сценариях [1], включая простые, такие как вы описываете.

Однако foreach имеет некоторые не связанные с производительностью преимущества по сравнению с for:

  1. Удобство .Вам не нужно хранить дополнительный локальный i (что не имеет смысла в жизни, кроме облегчения цикла), и вам не нужно извлекать текущее значение в переменную самостоятельно;конструкция цикла уже позаботилась об этом.
  2. Согласованность foreach вы можете выполнять итерации последовательностей, которые не являются массивами, с такой же легкостью.Если вы хотите использовать for для циклического выполнения упорядоченной последовательности, не являющейся массивом (например, карта / словарь), то вы должны написать код немного по-другому.foreach одинаков во всех случаях, которые он охватывает.
  3. Безопасность .С большой властью приходит большая ответственность.Зачем открывать возможности для ошибок, связанных с увеличением переменной цикла, если она вам не нужна?

Итак, как мы видим, foreach «лучше» использовать в большинство ситуаций.

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

[1] «Во всех сценариях» действительно означает «все сценарии»где коллекция удобна для повторения », что фактически будет« большинством сценариев »(см. комментарии ниже).Я действительно думаю, что итерационный сценарий, включающий коллекцию недружественных итераций, должен быть, однако, спроектирован.

10 голосов
/ 08 декабря 2010

Вероятно, вам следует учитывать и LINQ, если вы ориентируетесь на C # в качестве языка, поскольку это еще один логический способ создания циклов.

С помощью выполните операцию для каждого элемента в списке Вы имеете в виду изменить его на месте в списке или просто сделать что-то с элементом (например, распечатать его, накопить, изменить его и т. д.)?Я подозреваю, что это последнее, так как foreach в C # не позволит вам изменить коллекцию, которую вы зацикливаете, или, по крайней мере, не удобным способом ...

Вот две простые конструкции,сначала с помощью for, а затем с помощью foreach, который посещает все строки в списке и превращает их в строки в верхнем регистре:

List<string> list = ...;
List<string> uppercase = new List<string> ();
for (int i = 0; i < list.Count; i++)
{
    string name = list[i];
    uppercase.Add (name.ToUpper ());
}

(обратите внимание, что используется конечное условие i < list.Count вместо i < lengthс некоторым прекомпьютером length константа считается хорошей практикой в ​​.NET, так как в любом случае компилятору придется проверять верхнюю границу, когда list[i] вызывается в цикле; если мое понимание верно, компилятор может в некоторых случаяхобстоятельства, позволяющие оптимизировать проверку верхней границы, которую она обычно выполняла бы).

Вот эквивалент foreach:

List<string> list = ...;
List<string> uppercase = new List<string> ();
foreach (name in list)
{
    uppercase.Add (name.ToUpper ());
}

Примечание: в основном, конструкция foreach может повторятьсялюбые IEnumerable или IEnumerable<T> в C #, а не только над массивами или списками.Поэтому количество элементов в коллекции может быть неизвестно заранее или даже может быть бесконечным (в этом случае вам, безусловно, придется включить в цикл какое-то условие завершения, иначе оно не выйдет).

Вот несколько эквивалентных решений, которые я могу придумать, выраженных с использованием C # LINQ (и которые вводят концепцию лямбда-выражения , в основном встроенную функцию, принимающую x и возвращающую x.ToUpper () в следующих примерах):

List<string> list = ...;
List<string> uppercase = new List<string> ();
uppercase.AddRange (list.Select (x => x.ToUpper ()));

Или со списком uppercase, заполненным его конструктором:

List<string> list = ...;
List<string> uppercase = new List<string> (list.Select (x => x.ToUpper ()));

Или то же самое с использованием функции ToList:

List<string> list = ...;
List<string> uppercase = list.Select (x => x.ToUpper ()).ToList ();

Или то же самое с выводом типа:

List<string> list = ...;
var uppercase = list.Select (x => x.ToUpper ()).ToList ();

или если вы не возражаете получить результат как IEnumerable<string> (перечисляемая коллекция строк), вы можете удалить ToList:

List<string> list = ...;
var uppercase = list.Select (x => x.ToUpper ());

Или, может быть, еще один с C # SQL-подобными ключевыми словами from и select, что полностью эквивалентно:

List<string> list = ...;
var uppercase = from name in list
                select name => name.ToUpper ();

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

Ваш второй вопрос, поиск элемента в списке, и желание выйти, когда этот элемент найден также может быть очень удобно реализовано с помощью LINQ.Вот пример цикла foreach:

List<string> list = ...;
string result = null;
foreach (name in list)
{
    if (name.Contains ("Pierre"))
    {
        result = name;
        break;
    }
}

Вот простой эквивалент LINQ:

List<string> list = ...;
string result = list.Where (x => x.Contains ("Pierre")).FirstOrDefault ();

или с синтаксисом запроса:

List<string> list = ...;

var results = from name in list
              where name.Contains ("Pierre")
              select name;
string result = results.FirstOrDefault ();

Перечисление results выполняется только по требованию , что означает, что фактически список будет повторяться только до тех пор, пока не будет выполнено условие, при вызове для него метода FirstOrDefault.

Я надеюсь, что это принесет больше контекста в дебаты for или foreach, по крайней мере, в мире .NET.

6 голосов
/ 08 декабря 2010

Как ответил Стюарт Голодец, это абстракция.

Если вы используете только i в качестве индекса, в отличие от использования значения i для какой-либо другой цели, например

   String[] lines = getLines();
   for( int i = 0 ; i < 10 ; ++i ) {
      System.out.println( "line " + i + lines[i] ) ;
   }

тогда нет необходимости знать текущее значение i, а возможность просто приводит к возможности ошибок:

   Line[] pages = getPages();
   for( int i = 0 ; i < 10 ; ++i ) {
        for( int j = 0 ; j < 10 ; ++i ) 
             System.out.println( "page " + i + "line " + j + page[i].getLines()[j];
   }

Как говорит Эндрю Кениг: «Абстракция - это избирательное невежество»; если вам не нужно знать детали того, как вы выполняете итерацию некоторой коллекции, найдите способ игнорировать эти детали, и вы напишите более надежный код.

4 голосов
/ 08 декабря 2010

Причины использования foreach:

  • Предотвращает появление ошибок (например, вы забыли набрать i++ в цикле for), которые могут вызвать сбой цикла.Есть много способов запутать петли, но не так много способов запутать петли foreach.
  • Это выглядит намного чище / менее загадочно.возможно в некоторых случаях (например, если у вас есть IEnumerable<T>, который нельзя индексировать, как IList<T>).

Причины использовать for:

  • Такие циклы имеют небольшое преимущество в производительности при переборе плоских списков (массивов), поскольку при использовании перечислителя не создается дополнительный уровень косвенности.(Однако это повышение производительности минимально.)
  • Объект, который вы хотите перечислить, не реализует IEnumerable<T> - foreach работает только с перечислимыми.
  • Другие специализированные ситуации;например, если вы копируете из одного массива в другой, foreach не даст вам индексную переменную, которую вы можете использовать для адресации в слот целевого массива.for - это единственное, что имеет смысл в таких случаях.

Два случая, которые вы перечисляете в своем вопросе, фактически идентичны при использовании любого цикла - в первом вы просто итерируете всепуть к концу списка, а во второй вы break;, как только вы нашли искомый предмет.

Просто, чтобы объяснить foreach далее, этот цикл:

IEnumerable<Something> bar = ...;

foreach (var foo in bar) {
    // do stuff
}

является синтаксическим сахаром для:

IEnumerable<Something> bar = ...;

IEnumerator<Something> e = bar.GetEnumerator();
try {
    Something foo;
    while (e.MoveNext()) {
        foo = e.Current;
        // do stuff
    }
} finally {
    ((IDisposable)e).Dispose();
}
3 голосов
/ 08 декабря 2010

Если вы выполняете итерацию по коллекции, которая реализует IEnumerable, более естественно использовать foreach , поскольку следующий член в итерации назначается в то же время, когда выполняется тест для достижения конца. Например.,

 foreach (string day in week) {/* Do something with the day ... */}

проще, чем

for (int i = 0; i < week.Length; i++) { day = week[i]; /* Use day ... */ }

Вы также можете использовать цикл для в собственной реализации IEnumerable вашего класса. Просто попросите вашу реализацию GetEnumerator () использовать ключевое слово C # yield в теле вашего цикла:

yield return my_array[i];
2 голосов
/ 08 декабря 2010

Java имеет оба типа циклов, на которые вы указали.Вы можете использовать любой из вариантов цикла for в зависимости от ваших потребностей.Ваша потребность может быть такой:

  1. Вы хотите повторно запустить индекс элемента поиска в списке.
  2. Вы хотите получить сам элемент.1009 * В первом случае вы должны использовать классический (стиль c) для цикла.но во втором случае вы должны использовать цикл foreach.

    Цикл foreach можно использовать и в первом случае.но в этом случае вам нужно поддерживать свой собственный индекс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...