Условия завершения цикла

Эти for-циклы являются одними из первых основных примеров формальных доказательств корректности алгоритмов. У них разные, но эквивалентные условия прекращения действия:

1   for ( int i = 0; i != N; ++i )

2   for ( int i = 0; i < N; ++i )

Разница становится очевидной в постусловиях:

  • Первый дает надежную гарантию того, что i == N после завершения цикла.

  • Второй дает лишь слабую гарантию того, что i >= N после завершения цикла, но вы можете предположить, что i == N.

Если по какой-либо причине приращение ++i когда-либо изменяется на что-то вроде i += 2, или если i изменяется внутри цикла, или если N отрицательный, программа может завершиться ошибкой:

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

  • Второй цикл завершится, и через некоторое время программа может выйти из строя из-за вашего неверного предположения о i == N. Он может выйти из строя далеко от цикла, вызвавшего ошибку, что затруднит обратное отслеживание. Или он может молча делать что-то неожиданное, что еще хуже.

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

Тем, кто упомянул, что i недоступен за пределами for, полезно отметить, что значение i при завершении может использоваться при рассуждении о программах. Например, если вы хотите установить P (N) с помощью цикла, который поддерживает инвариант P (i), тогда i == N дает вам P (N), а i> = N - нет.

mweerden 25.09.2008 17:03

+1 за хорошо написанный вопрос, который ясно выражает достоинства каждого варианта.

R.. GitHub STOP HELPING ICE 06.10.2010 12:23
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
2
4 619
13

Ответы 13

В C++ для общности предпочтительнее использовать тест !=. Итераторы в C++ имеют различные концепции, такие как итератор ввода, прямой итератор, двунаправленный итератор, итератор произвольного доступа, каждый из которых расширяет предыдущий с новыми возможностями. Для работы < требуется итератор произвольного доступа, тогда как != просто требует итератора ввода.

Мы не должны смотреть на счетчик изолированно - если по какой-либо причине кто-то изменил способ увеличения счетчика, они изменили бы условия завершения и результирующую логику, если это требуется для i == N.

Я бы предпочел второе условие, так как оно более стандартное и не приведет к бесконечному циклу.

Если вы доверяете своему коду, вы можете сделать то же самое.

Если вы хотите, чтобы ваш код был удобочитаемым и понятным (и, следовательно, более терпимым к переходу от кого-то, кого вы должны считать неуклюжим), я бы использовал что-то вроде:

for ( int i = 0 ; i >= 0 && i < N ; ++i) 

Я всегда использую №2, так как тогда вы можете быть уверены, что цикл завершится ... Полагаться на то, что он будет равен N после этого, полагаться на побочный эффект ... Разве вам не лучше использовать саму переменную N?

[править] Простите ... Я имел в виду №2

В общем, я бы предпочел

for ( int i = 0; i < N; ++i )

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

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

Я думаю, что большинство программистов используют второй, потому что он помогает понять, что происходит внутри цикла. Я могу смотреть на него и «знать», что я начну с 0 и определенно буду меньше N.

В первом варианте этого качества нет. Я могу посмотреть на это, и все, что я знаю, это то, что я начну с 0 и что он никогда не будет равен N. Не совсем так полезно.

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

Конечно, второй вариант также имеет то преимущество, что его используют все ссылки на C, которые я видел :-)

Я предпочитаю использовать №2 только потому, что стараюсь не расширять значение i за пределами цикла for. Если бы я отслеживал такую ​​переменную, я бы создал дополнительный тест. Некоторые могут сказать, что это избыточно или неэффективно, но это напоминает читателю о моем намерении: В этот момент я должен равняться N

@timyates - на побочные эффекты полагаться не стоит

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

Конечно, у него также есть немного ленивое преимущество, заключающееся в том, что для ввода на один символ меньше;)

Я бы также сказал, что на языке с разумными правилами области видимости, поскольку i объявлен внутри конструкции цикла, он не должен быть доступен вне цикла. Это уменьшит вероятность того, что i будет равно N в конце цикла ...

Я думал то же самое, наверняка большинство языков будет иметь i только в области видимости цикла. +1

Shaun Austin 25.09.2008 13:01

На Perl: for( my $i=0; $i<N; ++$i ){...}

Brad Gilbert 17.10.2009 18:57

Я думаю, вы очень хорошо заявили о разнице между ними. Однако у меня есть следующие комментарии:

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

  • Вы заявили i индекс в for, поэтому я бы не стал делать ставку на значение i после цикла. Примеры немного вводят в заблуждение, поскольку они косвенно предполагают, что for определенная петля. На самом деле это просто более удобный способ написания:

    // version 1
    { int i = 0;
      while (i != N) {
        ...
        ++i;
      }
    }
    

    Обратите внимание на то, что i не определен после блока.

Если бы программист знал, что все вышеперечисленное не сделал бы общего предположения о значении i и был бы достаточно мудр, чтобы выбрать i<N в качестве конечных условий, чтобы гарантировать, что условие выхода в конечном итоге будет выполнено.

Использование любого из вышеперечисленных в C# вызовет ошибку компилятора, если вы использовали i вне цикла

Иногда я предпочитаю это:

for (int i = 0; (i <= (n-1)); i++) { ... }

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

Другая версия:

for (int i = 1; (i <= n); i++) { ... }

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

Для общего программирования я предпочитаю

for ( int i = 0; i < N; ++i )

к

for ( int i = 0; i != N; ++i )

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

Я не верю, что этот аргумент, "у вас возникнет искушение предположить, что i == N", является правдой. Я никогда не делал этого предположения и не видел, чтобы другой программист делал это.

С моей точки зрения формальной проверки и анализа автоматического завершения я очень предпочитаю №2 (<). Достаточно легко отследить, что какая-то переменная увеличивается (до var = x, после var = x+n для некоторого неотрицательного числа n). Однако не так-то просто увидеть, что i==N в конечном итоге останется в силе. Для этого нужно сделать вывод, что i увеличивается ровно на 1 на каждом шаге, что (в более сложных примерах) может быть потеряно из-за абстракции.

Если вы подумаете о цикле, который увеличивается на два (i = i + 2), эта общая идея становится более понятной. Чтобы гарантировать завершение, теперь нужно знать, что i%2 == N%2, тогда как это не имеет значения при использовании < в качестве условия.

Другие вопросы по теме