Цикл foreach з розривом / поверненням проти циклу while з явним інваріантом та пост-умовою


17

Це найпопулярніший спосіб (мені здається) перевірити, чи є значення в масиві:

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

Однак у книзі, яку я багато років тому читав, напевно, Вірт або Дайкстра, було сказано, що цей стиль кращий (у порівнянні з циклом "час" з виходом всередину):

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
Наведені тут приклади коду поєднують кілька різних питань. Раннє та багаторазове повернення (яке для мене переходить до розміру методу (не показано)), пошук масиву (який вимагає обговорення за участю лямбда), передбачення проти прямого індексування ... Це питання було б більш зрозумілим і простішим для відповідь, якщо вона зосереджена на одному з цих питань одночасно.
Ерік Ейдт


1
Я знаю, що це приклади, але є мови, які мають API для обробки саме цього випадку. Тобтоcollection.contains(foo)
Берін Лорич

2
Ви можете знайти книгу і перечитати її зараз, щоб побачити, що вона насправді сказала.
Thorbjørn Ravn Andersen

1
"Краще" - високо суб'єктивне слово. Зважаючи на це, можна з першого погляду сказати, що робить перша версія. Те, що друга версія робить саме те, що потребує ретельного вивчення.
Девід Хаммен

Відповіді:


19

Я думаю, що для простих циклів, таких як ці, стандартний перший синтаксис набагато зрозуміліший. Деякі люди вважають кілька повернень заплутаним або кодовим запахом, але для шматка коду цей маленький, я не вірю, що це справжня проблема.

Це стає трохи більш дискусійним для складніших циклів. Якщо вміст циклу не може вміститися на вашому екрані і має декілька повернень у циклі, слід зробити аргумент, що кілька точок виходу можуть ускладнити підтримку коду. Наприклад, якщо вам довелося переконатися, що якийсь метод підтримки стану запускається перед виходом з функції, було б легко пропустити додавання його до одного з операторів повернення, і ви викликаєте помилку. Якщо всі кінцеві умови можна перевірити в циклі часу, у вас є лише одна точка виходу і ви можете додати цей код після неї.

Однак, з циклами особливо добре спробувати вкласти якомога більше логіки в окремі методи. Це дозволяє уникнути багатьох випадків, коли другий метод мав би переваги. Пісні петлі з чітко розділеною логікою будуть мати значення більше, ніж який із цих стилів ви використовуєте. Крім того, якщо більшість кодів вашої програми використовує один стиль, вам слід дотримуватися цього стилю.


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 існує тільки в межах циклу. Область застосування - це один дуже важливий спосіб впровадження структури в програмування.


1
Будь ласка, дивіться en.wikipedia.org/wiki/Structured_programming
ruakh

1
@ruakh Я не впевнений, що забрати з твого коментаря. Це трапляється настільки ж пасивно-агресивно, ніби моя відповідь проти того, що написано на сторінці вікі. Будь ласка, докладно.
null

"Структуроване програмування" - це термін мистецтва з певним значенням, і ОП об'єктивно вірно, що версія №2 відповідає правилам структурованого програмування, тоді як версія №1 цього не робить. З вашої відповіді здавалося, що ви не знайомі з терміном мистецтва, і ви тлумачите термін буквально. Я не впевнений, чому мій коментар сприймається як пасивно-агресивний; Я мав на увазі це просто як інформативний.
ruakh

@ruakh Я не погоджуюся, що версія №2 більше відповідає правилам у кожному аспекті і пояснила це у своїй відповіді.
null

Ви кажете "Я не згоден", ніби це була суб'єктивна річ, але це не так. Повернення зсередини циклу - це категоричне порушення правил структурованого програмування. Я впевнений, що багато любителів структурованого програмування є шанувальниками мінімально-масштабних змінних, але якщо зменшити область застосування змінної, відступивши від структурованого програмування, то ви відійшли від структурованого програмування, періоду, а зменшення обсягу змінної не скасовує що.
ruakh

2

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

  • Перший цикл відповідає просто на питання так / ні: "Чи містить масив об'єкт, який я шукаю?" Це робиться найкоротшим способом.

  • Другий цикл відповідає на запитання: "Якщо масив містить об'єкт, який я шукаю, то який індекс першого збігу?" Знову ж таки, це робиться найкоротшим чином.

Оскільки відповідь на друге питання дає суворо більше інформації, ніж відповідь на перше, ви можете вибрати відповідь на друге питання, а потім отримати відповідь на перше питання. Це те, що лініяreturn i < array.length; .

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

  • Використання першого варіанту петлі - чудово.
  • Зміна першого варіанту просто встановити boolзмінну та перерву - теж добре. (Уникає другогоreturn твердження; відповідь доступна у змінній замість повернення функції.)
  • Використання std::findпрекрасно (повторне використання коду!).
  • Однак явно кодування знахідки, а потім зменшення відповіді на а boolце не.

Було б добре, якби низовики залишили коментар ...
cmaster - відновити моніку

2

Я запропоную взагалі третій варіант:

return array.find(value);

Існує багато різних причин ітерації масиву: Перевірте, чи існує певне значення, перетворіть масив в інший масив, обчисліть сукупне значення, відфільтруйте деякі значення з масиву ... Якщо ви використовуєте звичайну для циклу, це незрозуміло на перший погляд конкретно, як використовується цикл for. Однак більшість сучасних мов мають багаті API на своїх структурах даних масиву, які роблять ці різні наміри дуже явними.

Порівняйте перетворення одного масиву в інший з циклом for:

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

і за допомогою mapфункції стилю JavaScript :

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 , використовують цей стиль функціональної взаємодії з масивами та подібними колекціями.

Взагалі, хоча я не думаю, що використовувати для циклів обов'язково неправильно, і це питання особистого смаку, я вважаю, що сильно віддаю перевагу використанню цього стилю роботи з масивами. Це пов'язано саме з підвищеною чіткістю визначення того, що робить кожен цикл. Якщо ваша мова має подібні функції або інструменти у своїх стандартних бібліотеках, пропоную вам також скористатися цим стилем!


2
Рекомендуючи array.findнапрошуємо запитання, тому що ми маємо обговорити найкращий спосіб втілення array.find. Якщо ви не використовуєте апаратне забезпечення із вбудованою findоперацією, нам потрібно написати цикл.
Бармар

2
@Barmar Я не згоден. Як я зазначив у своїй відповіді, багато мов, які широко використовуються, забезпечують функції, як findу їхніх стандартних бібліотеках. Безперечно, ці бібліотеки реалізовують findта її родичів, які використовують для циклів, але це хороша функція: вона абстрагує технічні деталі від споживача функції, що дозволяє програмісту не думати про ці деталі. Тож, хоча findце, ймовірно, реалізовано за допомогою циклу for, він все ще допомагає зробити код більш читабельним, а оскільки він часто є у стандартній бібліотеці, його використання не додає значних накладних витрат або ризику.
Кевін

4
Але програмний інженер повинен реалізувати ці бібліотеки. Чи не застосовуються ті ж принципи інженерії програмного забезпечення для авторів бібліотек, як програмісти програм? Питання про написання циклів взагалі - не найкращий спосіб пошуку елемента масиву певною мовою
Barmar

4
Інакше кажучи, пошук елемента масиву - просто простий приклад, який він використовував для демонстрації різних методів циклічного циклу.
Бармар

-2

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


3
PMar, ефективність обох циклів майже рівнозначна - вони обидва мають порівняння.
Данила П'ятов

1
Якщо хтось дійсно піклується про ефективність, користувався би більш швидким алгоритмом. наприклад, сортуйте масив та виконайте двійковий пошук або використовуйте Hashtable.
user949300

Данила - ти не знаєш, яка структура даних стоїть за цим. Ітератор завжди швидкий. Індексований доступ може бути лінійним часом, а довжина навіть не повинна існувати.
gnasher729
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.