Цикл Foreach с циклом break / return против цикла while с явным инвариантом и постусловием

17

Это наиболее популярный способ (мне кажется) проверки, находится ли значение в массиве:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Однако в книге, которую я читал много лет назад, вероятно, Виртом или Дейкстрой, было сказано, что этот стиль лучше (по сравнению с циклом while с выходом внутри):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

Таким образом, дополнительное условие выхода становится явной частью инварианта цикла, в цикле нет скрытых условий и выходов, все становится более очевидным и более структурированным. Я обычно предпочитал этот последний шаблон всякий раз, когда это было возможно, и использовал for-loop только для итерации от aдо b.

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

Может быть, кто-то может дать хорошее обоснование в пользу одного из методов?

Обновление: это не вопрос множественных точек возврата функции, лямбда-выражения или поиска элемента в массиве как такового. Речь идет о том, как писать циклы с более сложными инвариантами, чем одно неравенство.

Обновление: хорошо, я вижу точку зрения людей, которые отвечают и комментируют: я смешал здесь цикл foreach, который сам по себе уже гораздо более понятен и читаем, чем цикл while. Я не должен был этого делать. Но это также интересный вопрос, поэтому давайте оставим его как есть: цикл foreach и дополнительное условие внутри, или цикл while с явным инвариантом цикла и постусловием после. Кажется, что цикл foreach с условием и выходом / разрывом выигрывает. Я создам дополнительный вопрос без цикла foreach (для связанного списка).

Данила Пятов
источник
2
Приведенные здесь примеры кода смешивают несколько разных проблем. Ранние и множественные возвраты (которые для меня относятся к размеру метода (не показан)), поиск по массиву (который требует обсуждения с участием лямбда-выражений), foreach или прямая индексация ... Этот вопрос будет более понятным и более простым для решения ответьте, если он сосредоточен только на одном из этих вопросов одновременно.
Эрик Эйдт
1
Я знаю, что это примеры, но есть языки, в которых есть API для обработки именно этого варианта использования. Т.е.collection.contains(foo)
Берин Лорич
2
Возможно, вы захотите найти книгу и перечитать ее сейчас, чтобы увидеть, что она на самом деле сказала.
Торбьерн Равн Андерсен
1
«Лучше» - это очень субъективное слово. Тем не менее, можно сразу увидеть, что делает первая версия. То, что вторая версия делает то же самое, требует некоторого изучения.
Дэвид Хаммен

Ответы:

19

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

Это становится немного более спорным для более сложных циклов. Если содержимое цикла не помещается на вашем экране и имеет несколько возвратов в цикле, необходимо сделать аргумент, что несколько точек выхода могут усложнить поддержку кода. Например, если вы должны были убедиться, что какой-либо метод поддержания состояния выполнялся до выхода из функции, было бы легко пропустить добавление его в один из операторов return, и вы могли бы вызвать ошибку. Если все конечные условия можно проверить в цикле while, у вас есть только одна точка выхода, и вы можете добавить этот код после нее.

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

Нафанаил
источник
56

Это легко.

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

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

Есть ZERO DOUBT, который является лучшим стилем кодирования (первый намного лучше).

Теперь - то, что ЯСНО для людей, может варьироваться от человека к человеку. Я не уверен, что существуют какие-либо объективные стандарты для этого (хотя публикация на таком форуме и получение информации от различных людей может помочь).

В этом конкретном случае, однако, я могу сказать вам, почему первый алгоритм более понятен: я знаю, как выглядит и делает итерация C ++ по синтаксису контейнера. Я усвоил это. Кто-то UNFAMILIAR (его новый синтаксис) с таким синтаксисом может предпочесть второй вариант.

Но как только вы знаете и понимаете этот новый синтаксис, его базовая концепция, которую вы можете просто использовать. При использовании итерационного цикла (второго) вы должны тщательно проверить, что пользователь ПРАВИЛЬНО проверяет все граничные условия для цикла по всему массиву (например, меньше, чем вместо «меньше или равно», тот же индекс, используемый для теста и для индексации и т. д.).

Льюис Прингл
источник
4
Новое относительно, как уже было в стандарте 2011 года. Кроме того, вторая демонстрация, очевидно, не C ++.
Дедупликатор
Если вы хотите использовать одну точку выхода, альтернативным решением было бы установить флаг longerLength = true, затем return longerLength.
Cullub
@Deduplicator Почему не вторая демоверсия C ++? Я не понимаю, почему нет или я упускаю что-то очевидное?
Rakete1111
2
@ Rakete1111 Необработанные массивы не имеют таких свойств, как length. Если бы он был фактически объявлен как массив, а не как указатель, они могли бы использовать sizeofили, если это было бы std::array, правильная функция-член size(), lengthсвойства не существует .
IllusiveBrian
@IllusiveBrian: sizeofбудет в байтах ... Самый общий с C ++ 17 std::size().
Дедупликатор
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[…] Все более очевидно и более структурировано.

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

значение NULL
источник
1
Пожалуйста, смотрите en.wikipedia.org/wiki/Structured_programming
ruakh
1
@ruakh Я не уверен, что отнять от вашего комментария. Это выглядит несколько пассивно-агрессивным, как будто мой ответ противоречит тому, что написано на вики-странице. Пожалуйста, дополните.
ноль
«Структурированное программирование» - это термин искусства с особым значением, и ОП объективно верен, что версия № 2 соответствует правилам структурированного программирования, а версия № 1 - нет. Из вашего ответа стало ясно, что вы не знакомы с термином «искусство» и толковали его буквально. Я не уверен, почему мой комментарий выглядит пассивно-агрессивным; Я имел в виду это просто как информативно.
Руах
@ruakh Я не согласен с тем, что версия №2 более соответствует всем правилам и объяснил это в своем ответе.
ноль
Вы говорите «я не согласен», как будто это субъективная вещь, но это не так. Возвращение из цикла является категорическим нарушением правил структурированного программирования. Я уверен, что многие энтузиасты структурированного программирования являются поклонниками переменных с минимальной областью действия, но если вы уменьшите область действия переменной, выйдя из структурного программирования, то вы отошли от структурного программирования, период, и сокращение области действия переменной не отменяет тот.
Руах
2

Два цикла имеют разную семантику:

  • Первый цикл просто отвечает на простой вопрос «да / нет»: «Содержит ли массив искомый объект?» Это делается максимально кратко.

  • Второй цикл отвечает на вопрос: «Если массив содержит искомый объект, каков индекс первого совпадения?» Опять же, это делается максимально кратко.

Так как ответ на второй вопрос содержит строго больше информации, чем ответ на первый, вы можете выбрать ответ на второй вопрос и затем получить ответ на первый вопрос. Это то, что линияreturn i < array.length; делает , в любом случае.

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

  • Использование первого варианта цикла нормально.
  • Изменение первого варианта просто установить bool переменную и разрыв, тоже хорошо. (Избегает второго returnутверждения, ответ доступен в переменной вместо возврата функции.)
  • С помощью std::find в порядке (повторное использование кода!).
  • Тем не менее, явное кодирование находки, а затем приведение ответа к boolнет.
cmaster - восстановить монику
источник
Было бы неплохо, если бы downvoters оставил комментарий ...
cmaster - восстановить монику
2

Я предложу третий вариант в целом:

return array.find(value);

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

Сравните преобразование одного массива в другой с помощью цикла for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

и используя функцию в стиле JavaScript map:

array.map((value) => value * 2);

Или суммирование массива:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

против:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Сколько времени вам нужно, чтобы понять, что это делает?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

против

array.filter((value) => (value >= 0));

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

Большинство современных языков, включая JavaScript , Ruby , C # и Java , используют этот стиль функционального взаимодействия с массивами и подобными коллекциями.

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

Kevin
источник
2
Рекомендация array.findнапрашивается на вопрос, так как мы должны обсудить наилучший способ реализации array.find. Если вы не используете аппаратное обеспечение со встроенной findоперацией, мы должны написать цикл там.
Бармар
2
@ Бармар, я не согласен. Как я указывал в своем ответе, многие широко используемые языки предоставляют функции, как findв их стандартных библиотеках. Несомненно, эти библиотеки реализуют findи используют его род для циклов, но это то, что делает хорошая функция: она абстрагирует технические детали от потребителя функции, позволяя программисту не думать об этих деталях. Таким образом, несмотря на то, что findон, вероятно, реализован с помощью цикла for, он все же помогает сделать код более читабельным, и поскольку он часто находится в стандартной библиотеке, его использование не приводит к значительным накладным расходам или риску.
Кевин
4
Но программист должен реализовать эти библиотеки. Разве те же принципы разработки программного обеспечения не применяются к авторам библиотеки, как программисты приложений? Вопрос в том, чтобы писать циклы в целом, а не в лучшем способе поиска элемента массива на определенном языке
Barmar
4
Иными словами, поиск элемента массива - это всего лишь простой пример, который он использовал для демонстрации различных методов зацикливания.
Бармар
-2

Все сводится к тому, что именно означает «лучше». Для практических программистов это обычно означает эффективность - т. Е. В этом случае выход непосредственно из цикла исключает одно дополнительное сравнение, а возврат логической константы позволяет избежать повторного сравнения; это экономит циклы. Дейкстра больше заботится о создании кода, который легче доказать, что он правильный . [мне показалось, что CS-образование в Европе относится к «проверке правильности кода» гораздо серьезнее, чем CS-образование в США, где экономические силы имеют тенденцию доминировать в практике кодирования]

PMar
источник
3
PMar, с точки зрения производительности оба цикла в значительной степени эквивалентны - у них есть два сравнения.
Данила Пятов
1
Если бы кто-то действительно заботился о производительности, он использовал бы более быстрый алгоритм. например, отсортируйте массив и выполните бинарный поиск или используйте Hashtable.
user949300
Данила - ты не знаешь, какая структура данных стоит за этим. Итератор всегда быстр. Индексированный доступ может иметь линейное время, а продолжительность даже не должна существовать.
gnasher729