Эти 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. Он может выйти из строя далеко от цикла, вызвавшего ошибку, что затруднит обратное отслеживание. Или он может молча делать что-то неожиданное, что еще хуже.
Какое условие прерывания вы предпочитаете и почему? Есть другие соображения? Почему многие программисты, которые это знают, отказываются применять?
+1 за хорошо написанный вопрос, который ясно выражает достоинства каждого варианта.





В 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
На Perl: for( my $i=0; $i<N; ++$i ){...}
Я думаю, вы очень хорошо заявили о разнице между ними. Однако у меня есть следующие комментарии:
Это не "языково-независимый", я вижу, что ваши примеры написаны на 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, тогда как это не имеет значения при использовании < в качестве условия.
Тем, кто упомянул, что i недоступен за пределами for, полезно отметить, что значение i при завершении может использоваться при рассуждении о программах. Например, если вы хотите установить P (N) с помощью цикла, который поддерживает инвариант P (i), тогда i == N дает вам P (N), а i> = N - нет.