Тестовые петли сверху или снизу? (в то время как против делать пока)
Когда я брал CS в колледже (середина 80-х), одна из идей, которые постоянно повторялись, заключалась в том, чтобы всегда писать циклы, которые проверяют сверху (в то время как...), а не внизу (делать... пока) из петля. Эти понятия часто подкреплялись ссылками на исследования, которые показали, что циклы, которые тестировались сверху, были статистически гораздо более правильными, чем их аналоги, тестируемые снизу.
В результате я почти всегда пишу циклы, которые проверяют сверху. Я не делаю этого, если это вносит дополнительную сложность в код, но этот случай кажется редким. Я заметил, что некоторые программисты обычно пишут циклы, которые тестируют внизу. Когда я вижу такие конструкции, как:
if (condition)
{
do
{
...
} while (same condition);
}
или обратное (if
внутри while
), это заставляет меня задаться вопросом, действительно ли они написали это таким образом или они добавили if
заявление, когда они поняли, что цикл не обрабатывает нулевой регистр.
Я немного погуглил, но не смог найти никакой литературы на эту тему. Как вы, ребята (и девочки) пишете свои петли?
31 ответ
Я всегда придерживаюсь правила, что если он должен выполняться ноль или более раз, тестируйте в начале, если он должен выполняться один или несколько раз, тестируйте в конце. Я не вижу никакой логической причины использовать код, который вы перечислили в своем примере. Это только добавляет сложности.
Используйте циклы while, когда вы хотите проверить условие перед первой итерацией цикла.
Используйте циклы do-while, когда вы хотите проверить условие после выполнения первой итерации цикла.
Например, если вы обнаружите, что делаете что-то вроде одного из этих фрагментов:
func();
while (condition) {
func();
}
//or:
while (true){
func();
if (!condition) break;
}
Вы должны переписать это как:
do{
func();
} while(condition);
Разница заключается в том, что цикл do выполняет "сделать что-то" один раз, а затем проверяет условие, чтобы увидеть, следует ли ему повторить "сделать что-то", в то время как цикл while проверяет условие перед выполнением чего-либо
Избегает
do
/while
действительно помочь сделать мой код более читабельным?
Нет.
Если имеет больше смысла использовать do
/while
цикл, затем сделайте это. Если вам нужно выполнить тело цикла один раз перед проверкой условия, тогда do
/while
Цикл, вероятно, самая простая реализация.
Первый может не выполняться вообще, если условие ложно. Другой выполнит хотя бы один раз, затем проверит условие.
Ради удобочитаемости кажется разумным тестировать сверху. Факт, что это петля, важен; Человек, читающий код, должен знать условия цикла, прежде чем пытаться понять тело цикла.
Вот хороший пример из реальной жизни, с которым я недавно столкнулся. Предположим, у вас есть несколько задач обработки (например, обработка элементов в массиве), и вы хотите разделить работу между одним потоком на каждое ядро ЦП. Для запуска текущего кода должно быть хотя бы одно ядро! Таким образом, вы можете использовать do... while
что-то вроде:
do {
get_tasks_for_core();
launch_thread();
} while (cores_remaining());
Это почти ничтожно мало, но, возможно, стоит подумать о выигрыше в производительности: его можно было бы написать как стандарт while
цикл, но это всегда будет делать ненужное начальное сравнение, которое всегда будет оценивать true
- и на одноядерном, условие do-while ветвится более предсказуемо (всегда ложно, вместо чередования истина / ложь для стандарта while
).
Даааа... это правда.. делай пока побежит хотя бы один раз. Это единственная разница. Больше нечего обсуждать
Да, так же, как использование for вместо while или foreach вместо for улучшает читабельность. Тем не менее, некоторые обстоятельства нужно делать какое-то время, и я согласен, что было бы глупо заставлять эти ситуации замыкаться.
Цикл while сначала проверит "условие"; если оно ложно, оно никогда не "сделает что-то". Но цикл do...while сначала "что-то сделает", а затем проверит "условие".
Более полезно думать с точки зрения общего использования. Подавляющее большинство циклов while работают вполне естественно с while
даже если их можно заставить работать do...while
так что в основном вы должны использовать его, когда разница не имеет значения. Я бы таким образом использовал do...while
для редких сценариев, где это обеспечивает заметное улучшение читаемости.
Первая проверяет условие перед выполнением, поэтому возможно, что ваш код никогда не будет вводиться под кодом. Второй выполнит код в течение до тестирования условия.
Как отмечает Piemasons, разница заключается в том, выполняется ли цикл один раз перед выполнением теста, или тест выполняется сначала, чтобы тело цикла могло никогда не выполняться.
Ключевой вопрос, который имеет смысл для вашего приложения.
Взять два простых примера:
Допустим, вы просматриваете элементы массива. Если массив не имеет элементов, вы не хотите обрабатывать номер один из нуля. Так что вы должны использовать WHILE.
Вы хотите отобразить сообщение, принять ответ, и если ответ недействителен, спрашивайте снова, пока не получите действительный ответ. Так что вы всегда хотите спросить один раз. Вы не можете проверить, является ли ответ действительным, пока не получите ответ, поэтому вам придется пройти тело цикла один раз, прежде чем вы сможете проверить условие. Вы должны использовать DO/WHILE.
Для тех, кто не может придумать причину для цикла "один или более раз":
try {
someOperation();
} catch (Exception e) {
do {
if (e instanceof ExceptionIHandleInAWierdWay) {
HandleWierdException((ExceptionIHandleInAWierdWay)e);
}
} while ((e = e.getInnerException())!= null);
}
То же самое можно использовать для любой иерархической структуры.
в классе Node:
public Node findSelfOrParentWithText(string text) {
Node node = this;
do {
if(node.containsText(text)) {
break;
}
} while((node = node.getParent()) != null);
return node;
}
A while () проверяет условие перед каждым выполнением тела цикла, а do... while () проверяет условие после каждого выполнения тела цикла.
Таким образом, **do...while()**s всегда будет выполнять тело цикла хотя бы один раз.
Функционально while () эквивалентно
startOfLoop:
if (!condition)
goto endOfLoop;
//loop body goes here
goto startOfLoop;
endOfLoop:
и do... while () эквивалентно
startOfLoop:
//loop body
//goes here
if (condition)
goto startOfLoop;
Обратите внимание, что реализация, вероятно, более эффективна, чем эта. Однако do... while () включает в себя на одно сравнение меньше, чем while (), поэтому оно немного быстрее. Используйте do... while (), если:
- Вы знаете, что условие всегда будет верным в первый раз, или
- Вы хотите, чтобы цикл выполнялся один раз, даже если условие ложно для начала.
Я, как правило, предпочитаю делать циклы пока. Если условие всегда будет истинным в начале цикла, я предпочитаю проверить его в конце. На мой взгляд, весь смысл условий тестирования (кроме утверждений) заключается в том, что никто не знает результат теста. Если я вижу цикл while с условным тестом вверху, я склонен рассмотреть случай, когда цикл выполняется ноль раз. Если этого никогда не произойдет, почему бы не написать код, который бы это ясно показал?
Вот перевод:
do { y; } while(x);
Такой же как
{ y; } while(x) { y; }
Обратите внимание, что дополнительный набор скобок предназначен для случая, когда у вас есть определения переменных в y
, Их объем должен быть локальным, как в случае с циклом do. Итак, цикл do-while просто выполняет свое тело хотя бы один раз. Кроме того, две петли идентичны. Так что, если мы применим это правило к вашему коду
do {
// do something
} while (condition is true);
Соответствующий цикл while для вашего do-loop выглядит так:
{
// do something
}
while (condition is true) {
// do something
}
Да, вы видите соответствующий цикл while для вашего do отличается от вашего while:)
Варианты использования различны для двух. Это не вопрос "лучших практик".
Если вы хотите, чтобы цикл выполнялся исключительно на основе условия, используйте для или пока
Если вы хотите сделать что-то один раз, независимо от условия, а затем продолжить делать это на основе оценки условия.делать пока
Оба соглашения верны, если вы знаете, как правильно написать код:)
Обычно использование второго соглашения (do {} while ()) предназначено для того, чтобы избежать дублирования оператора вне цикла. Рассмотрим следующий (упрощенный) пример:
a++;
while (a < n) {
a++;
}
можно написать более кратко, используя
do {
a++;
} while (a < n)
Конечно, этот конкретный пример может быть написан еще более кратко, как (предполагая синтаксис C)
while (++a < n) {}
Но я думаю, что вы можете увидеть смысл здесь.
while( someConditionMayBeFalse ){
// this will never run...
}
// then the alternative
do{
// this will run once even if the condition is false
while( someConditionMayBeFalse );
Разница очевидна и позволяет вам запускать код, а затем оценивать результат, чтобы увидеть, нужно ли вам "Делать это снова", а другой метод while позволяет игнорировать блок сценария, если условие не выполняется.
Это на самом деле предназначено для разных вещей. В C вы можете использовать конструкцию do - while для достижения обоих сценариев (выполняется по крайней мере один раз и выполняется при true). Но PASCAL имеет повторение - до и во время для каждого сценария, и, если я правильно помню, у ADA есть другая конструкция, которая позволяет вам выйти посередине, но, конечно, это не то, что вы просите. Мой ответ на ваш вопрос: мне нравится мой цикл с тестированием сверху.
На самом деле это не ответ, а повторение того, что сказал один из моих лекторов, и это заинтересовало меня в то время.
Два типа цикла while..do и do.. while на самом деле являются экземплярами третьего более общего цикла, в котором тест находится где-то посередине.
begin loop
<Code block A>
loop condition
<Code block B>
end loop
Кодовый блок A выполняется по крайней мере один раз, а B выполняется ноль или более раз, но не выполняется на самой последней (неудачной) итерации. Цикл while - это когда кодовый блок a пуст, а do.. while - это когда кодовый блок b пуст. Но если вы пишете компилятор, вам может быть интересно обобщить оба случая в такой цикл.
Я предполагаю, что некоторые люди проводят тесты внизу, потому что вы могли бы сэкономить один или несколько машинных циклов, сделав это 30 лет назад.
Это действительно зависит от того, есть ли ситуации, когда вы хотите провести тестирование сверху, другие, когда вы хотите провести тестирование внизу, и другие, когда вы хотите провести тестирование в середине.
Однако приведенный пример кажется абсурдным. Если вы собираетесь тестировать сверху, не используйте оператор if и тестируйте снизу, просто используйте оператор while, для этого он и создан.
Я пишу свои в основном исключительно наверху. Это меньше кода, поэтому для меня, по крайней мере, меньше возможностей что-то испортить (например, при вставке условия копирования в двух местах вам всегда придется его обновлять)
Чтобы написать код, который является правильным, в основном нужно выполнить мысленное, возможно, неофициальное доказательство правильности.
Чтобы доказать правильность цикла, стандартным способом является выбор инварианта цикла и доказательство индукции. Но пропустите сложные слова: то, что вы делаете, неофициально, это выясняет что-то, что верно для каждой итерации цикла, и что когда цикл завершен, то, что вы хотели выполнить, теперь верно. Инвариант цикла является ложным в конце, чтобы цикл завершился.
Если условия цикла довольно легко отображаются на инвариант, а инвариант находится в верхней части цикла, и можно сделать вывод, что инвариант является истинным на следующей итерации цикла, работая с кодом цикла, то это легко выяснить, что цикл правильный.
Однако, если инвариант находится в нижней части цикла, тогда, если у вас нет утверждения непосредственно перед циклом (хорошая практика), тогда это становится более трудным, потому что вы должны по сути сделать вывод, каким должен быть этот инвариант, и что любой код то, что выполняется до цикла, делает инвариант цикла истинным (поскольку нет предварительного условия цикла, код будет выполняться в цикле). Просто становится все труднее доказать правильность, даже если это неформальное доказательство в вашей голове.
Сначала вы должны думать о тесте как о части кода цикла. Если тест логически принадлежит в начале обработки цикла, то это тест вершины цикла. Если тест логически принадлежит в конце цикла (т. Е. Он решает, должен ли цикл продолжать работать), то это, вероятно, тест конца цикла.
Вам придётся сделать что-то причудливое, если тест по логике принадлежит им посередине.:-)
Исходя из моих ограниченных знаний о генерации кода, я думаю, что было бы неплохо написать нижние тестовые циклы, поскольку они позволяют компилятору лучше выполнять оптимизацию циклов. Для нижних тестовых циклов гарантируется, что цикл выполняется хотя бы один раз. Это означает, что инвариантный код цикла "доминирует" на узле выхода. И, таким образом, может быть безопасно перемещен непосредственно перед началом цикла.
Как правило, это зависит от того, как вы структурируете свой код. И, как кто-то уже ответил, для некоторых алгоритмов требуется хотя бы одна итерация. Таким образом, чтобы избежать дополнительных флагов подсчета итераций или, по крайней мере, одного произошедшего взаимодействия, используйте do/while.
В типичном классе дискретных структур по информатике это простое доказательство того, что между ними существует сопоставление эквивалентности.
Стилистически я предпочитаю while (easy-expr) { }, когда easy-expr известен заранее и готов к работе, и цикл не имеет много повторяющихся накладных расходов / инициализации. Я предпочитаю делать { } while (несколько-менее-легкий-expr); когда повторяется дополнительная нагрузка, и условие может быть не так просто установить заранее. Если я пишу бесконечный цикл, я всегда использую while (true) { }. Я не могу объяснить почему, но мне просто не нравится писать для (;;) { }.