Является ли while (true) с break плохой практикой программирования?

Я часто использую этот шаблон код:

while(true) {

    //do something

    if(<some condition>) {
        break;
    }

}   

другой программист сказал мне, что это плохая практика и что я должен заменить ее более стандартной:

while(!<some condition>) {

    //do something

}   

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

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

может ли кто-нибудь обогатить этот аргумент, добавив доказательства для одной стороны или другой?

23 ответов


есть разница между двумя примерами. Первый будет выполнять "что-то", по крайней мере, однажды каждый раз, даже если утверждение не верно. Второй будет "делать что-то" только тогда, когда утверждение оценивается как true.

Я думаю, что вы ищете цикл do-while. Я на 100% согласен, что while (true) не является хорошей идеей, потому что это затрудняет поддержание этого кода, и то, как вы избегаете цикла, очень goto esque, который считается плохим практиковать.

попробуй:

do {
  //do something
} while (!something);

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


процитировать, что отметил разработчик days gone by, Wordsworth:

...
Воистину, тюрьма, в которую мы обречены!--8--> Сами по себе никакой тюрьмы нет; и поэтому для меня,
В разных настроениях это было времяпрепровождение.--8--> В пределах скудного участка земли сонета;
Приятно если некоторые души (для таких их нужд должно быть)
Кто почувствовал тяжесть слишком большой свободы,--8--> Должен найти там краткое утешение, как и я. найдено.

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

while (true) {
    action0;
    if (test0) break;
    action1;
}

это то, что легко позволить action0 и action1 становитесь все большими и большими кусками кода или добавляйте "еще одну" последовательность тестовых действий, пока не станет трудно указать на определенную строку и ответить на вопрос: "какие условия я знаю на данный момент?" Так, не создавая правил для других программистов, я стараюсь избегать while (true) {...} идиома в моем собственном коде, когда это возможно.


когда вы можете написать свой код в форме

while (condition) { ... }

или

while (!condition) { ... }

С нет выходов (break, continue или goto) в организме, эта форма предпочтительна, потому что кто-то может прочитать код и поймите условие завершения, просто взглянув на заголовок. Это хорошо.

но много петель не подходят для этой модели, и бесконечный цикл с явным выходом(выходами) посередине является почетным модель. (Петли с continue обычно труднее понять, чем петли с break.) Если вы хотите привести какие-то доказательства или авторитетные источники, не смотрите дальше знаменитой статьи Дона кнута о структурированное Программирование с помощью операторов Goto; вы найдете все примеры, Аргументы и объяснения, которые вы могли бы хотеть.

незначительный пункт идиомы: написание while (true) { ... } клеймит вас как старый программист Pascal или, возможно, в наши дни программист Java. Если вы пишете в C или C++, выбранный фразеологизм

for (;;) { ... }

для этого нет веской причины, но вы должны написать это так, потому что именно так программисты C ожидают увидеть это.


предпочитаю

while(!<some condition>) {
    //do something
}

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

аргумент, который я имею против использования break чтобы выйти из бесконечного цикла, вы по существу используете break заявление goto. Я не религиозно против использования goto (если язык поддерживает его, это честная игра), но я пытаюсь заменить его, если есть более читаемая альтернатива.

в случае многих break очки я бы заменил их на

while( !<some condition> ||
       !<some other condition> ||
       !<something completely different> ) {
    //do something
}

консолидация всех условий остановки, таким образом делает его гораздо легче увидеть, чем закончится этот цикл. break заявления могут быть разбросаны вокруг, и это что угодно, но не читается.


while (true) может иметь смысл, если у вас много операторов, и вы хотите остановить, если какой-либо сбой

 while (true) {
     if (!function1() ) return;   
     if (!function2() ) return;   
     if (!function3() ) return;   
     if (!function4() ) return;  
   }

лучше, чем

 while (!fail) {
     if (!fail) {
       fail = function1()
     }           
     if (!fail) {
       fail = function2()
     }  
     ........

   }

Хавьер сделал интересный комментарий к моему предыдущему ответу (тот, который цитировал Вордсворта):

я думаю, что while(true){} является более "чистой" конструкцией, чем while (condition){}.

и я не мог адекватно ответить на 300 символов (извините!)

в моем обучении и наставничестве я неофициально определил " сложность "как" сколько остального кода мне нужно иметь в голове, чтобы понять эту единственную строку или выражение?" Чем больше я должен иметь в виду, тем сложнее код. Чем больше код говорит мне явно, тем менее сложным.

Итак, с целью уменьшения сложности, позвольте мне ответить Хавьеру с точки зрения полноты и силы, а не чистоты.

я думаю об этом фрагменте кода:

while (c1) {
    // p1
    a1;
    // p2
    ...
    // pz
    az;
}

как выражение двух вещей одновременно:

  1. (весь) тело будет повторяться до тех пор, пока c1 остается верным, и
  2. в точке 1, где a1 выполнено, c1 is гарантированный держать.

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

давайте поставим конкретный (крошечный) пример, чтобы подумать о состоянии и первом действии:

while (index < length(someString)) {
    // p1
    char c = someString.charAt(index++);
    // p2
    ...
}

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

"внутренняя" проблема заключается в том, что нам гарантировано, что действие, следующее за пунктом 1, будет законным, поэтому, читая код в пункте 2, я могу думать о том, что делается со значением char I знаю было законно получено. (Мы даже не можем оценить состояние, если someString является нулевым ref, но я также предполагаю, что мы защитили от этого в контекст вокруг этого примера!)

напротив, петля формы:

while (true) {
    // p1
    a1;
    // p2
    ...
}

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

на внутреннем уровне, у меня есть абсолютно нет явная гарантия о любых обстоятельствах, которые могут иметь место в пункте 1. Условие true, что, конечно, верно везде, является самым слабым утверждением о том, что мы можем знать в любой момент в программе. Понимание предварительных условий действия - очень ценная информация, когда вы пытаетесь думать о том, что делает действие!

Итак, я предлагаю while (true) ... идиома гораздо более неполная и слабая, а потому более сложная, чем while (c1) ... согласно логике, которую я описал выше.


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

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


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

So while (true)... не следует классифицировать как хорошее или плохое, пусть ситуация решит, что использовать


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

  • Это проще, так как вы не нужен еще один тест в коде.
  • это легче читать, так как вам не нужно охотиться за перерывом внутри цикла.
  • вы изобретаете колесо. Весь смысл в том, чтобы делать что-то, пока тест верен. Зачем подрывать это, поставив условие перерыва где-то еще?

Я бы использовал цикл while(true), если бы я писал демона или другой процесс, который должен работать, пока он не будет убит.


Если есть одно (и только одно) не исключительное условие разрыва, предпочтительнее поместить это условие непосредственно в конструкцию потока управления (в то время как). Видя while (true) { ... } заставляет меня как читателя кода думать, что нет простого способа перечислить условия разрыва и заставляет меня думать: "внимательно посмотрите на это и тщательно подумайте об условиях разрыва (что установлено перед ними в текущем цикле и что могло быть установлено в предыдущем цикле)"

In короче говоря, я с вашим коллегой в простейшем случае, но пока (true) {... } не редкость.


идеальный ответ консультанта: это зависит. В большинстве случаев правильная вещь - либо использовать цикл while

while (condition is true ) {
    // do something
}

или "повторить до", который выполняется на C-подобном языке с

do {
    // do something
} while ( condition is true);

если в любом из этих случаев работает, используйте их.

иногда, как во внутреннем цикле сервера, вы действительно имеете в виду, что программа должна продолжать работать, пока что-то внешнее не прерывает ее. (Рассмотрим, например, демон httpd - он не остановится, если он сбой или остановлено выключением.)

ТОГДА И ТОЛЬКО ТОГДА используйте некоторое время(1):

while(1) {
   accept connection
   fork child process
}

Final case-это редкий случай, когда вы хотите выполнить некоторую часть функции перед завершением. В этом случае используйте:

while(1) { // or for(;;)
   // do some stuff
   if (condition met) break;
   // otherwise do more stuff.
}

есть практически идентичный вопрос уже в SO at пока так...перерыв...конец, а хороший дизайн?. @Glomek ответил (в недооцененном посте):

иногда это очень хороший дизайн. См.Структурированное Программирование С Заявлениями Goto Дональд Кнут для некоторых примеров. Я часто использую эту основную идею для циклов, которые запускают "n с половиной раз", особенно циклы чтения/процесса. Тем не менее, я обычно стараюсь иметь только одно заявление break. Этот делает его легче рассуждать о состоянии программы после завершения цикла.

несколько позже я ответил соответствующим, а также прискорбно недооцененным комментарием (отчасти потому, что я не заметил, что Гломек в первый раз, я думаю):

одна увлекательная статья - "структурированное Программирование с утверждениями go to" кнута от 1974 года (доступна в его книге "грамотное программирование", и, вероятно, в другом месте). Он обсуждает, среди прочего вещи, контролируемые способы выхода из циклов и (не используя термин) оператор loop-and-a-half.

Ada также предоставляет циклические конструкции, включая

loopname:
    loop
        ...
        exit loopname when ...condition...;
        ...
    end loop loopname;

исходный код вопроса аналогичен этому в intent.

одно различие между указанным элементом SO и это "окончательный разрыв"; это цикл с одним выстрелом, который использует break для выхода из цикла рано. Были ли вопросы о том, что это тоже хороший стиль - у меня нет перекрестной ссылки под рукой.


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


Это не столько while (true) часть это плохо, но тот факт, что вы должны сломать или выйти из него, это проблема. break и goto не являются действительно приемлемыми методами управления потоком.

Я также не вижу смысла. Даже в чем-то, что проходит через всю продолжительность программы, вы можете, по крайней мере, иметь как логическое значение Quit или что-то, что вы установили в true, чтобы выйти из цикла должным образом в цикле while(!Покидать.).. Не только вызывая перерыв в какой-то произвольной точке и выпрыгивая,


проблема в том, что не каждый алгоритм придерживается модели "while(cond){action}".

модель общего цикла выглядит так:

loop_prepare
 loop:
  action_A
  if(cond) exit_loop
  action_B
  goto loop
after_loop_code

когда нет action_A вы можете заменить его на :

loop_prepare
  while(cond)
  action_B
after_loop_code

когда нет action_B вы можете заменить его на :

loop_prepare
  do action_A
  while(cond)
after_loop_code

в общем случае action_A будет выполняться n раз и action_B будет выполняться (n-1) раз.

пример реальной жизни: печать всех элементов таблиц, разделенных запятыми. Нам нужны все n элементов с запятыми (n-1).

вы всегда можете сделать некоторые трюки, чтобы придерживаться модели while-loop, но это всегда будет повторять код или проверять дважды одно и то же условие (для каждого цикла) или добавлять новую переменную. Таким образом, вы всегда будете менее эффективными и менее читаемыми, чем модель цикла while-true-break.

пример (плохой) "трюк": добавить переменную и условие

loop_prepare
b=true // one more local variable : more complex code
 while(b): // one more condition on every loop : less efficient
  action_A
  if(cond) b=false // the real condition is here
  else action_B
after_loop_code

пример (плохой) "трюк": повторите код. Повторный код не должен быть забыт при изменении одного из двух разделов.

loop_prepare
action_A
 while(cond):
  action_B
  action_A
after_loop_code

Примечание: В последнем примере программист может запутать (добровольно или нет) код, смешав "loop_prepare" с первым "action_A" и action_B со вторым action_A. Чтобы у него было чувство, что он этого не делает.


Он, вероятно, прав.

функционально эти два могут быть одинаковыми.

на читабельности и понимание потока программы, в то время как(условие) лучше. Перерыв больше смахивает на Гото. While (условие) очень ясно для условий, которые продолжают цикл и т. д. Это не означает, что перерыв неправильный, просто может быть менее читаемым.

несколько преимуществ использования последней конструкции, которые приходят мне на ум:

  • легче понять, что делает цикл, не ища разрывов в коде цикла.

  • Если вы не используете другие разрывы в коде цикла, в вашем цикле есть только одна точка выхода, и это условие while ().

  • обычно заканчивается тем, что меньше кода, что добавляет читаемости.


Я предпочитаю пока(!) подход, потому что он более четко и сразу передает намерение цикла.


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

His reasoning was that you could "forget the break" too easily and have an endless loop.

в течение цикла while вы фактически просите процесс, который работает бесконечно, если что-то не произойдет, и если это что-то не произойдет в определенном параметре, вы получите именно то, что хотели... бесконечный цикл.


Я думаю, что преимущество использования " while (true)", вероятно, заключается в том, чтобы упростить запись нескольких условий выхода, особенно если эти условия выхода должны отображаться в другом месте в блоке кода. Однако для меня это может быть хаотично, когда мне нужно выполнить сухой код, чтобы увидеть, как код взаимодействует.

лично я постараюсь избежать пока (true). Причина в том, что всякий раз, когда я оглядываюсь на код, написанный ранее, я обычно обнаруживаю, что мне нужно выяснить, когда это работает/прерывается больше, чем на самом деле. Поэтому, чтобы найти "перерывы" сначала немного хлопотно для меня.

Если есть необходимость в нескольких условие выхода, я, как правило, рефакторинг состоянии определить логику в отдельную функцию, так что цикл блок выглядит чистым и более понятным.


то, что ваш друг рекомендует, отличается от того, что вы сделали. Свой код роднее

do{
    // do something
}while(!<some condition>);

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

но бывают моменты, когда перерывы в порядке, как упоминалось другими. В ответ на беспокойство вашего друга о "забудьте перерыв", я часто пишу в следующей форме:

while(true){
    // do something
if(<some condition>) break;
    // continue do something
}

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


использование циклов, таких как

while (1) {do stuff }

необходима в некоторых ситуациях. Если вы делаете какое-либо встроенное системное программирование (например, микроконтроллеры, такие как PICs, MSP430 и DSP), то почти весь ваш код будет через некоторое время(1) цикл. При кодировании для DSPs иногда вам просто нужно некоторое время (1) {}, а остальная часть кода-это подпрограмма службы прерываний (ISR).


мне нравится for петли лучше:

for (;;)
{
    ...
}

или даже

for (init();;)
{
    ...
}

но если init() функция выглядит как тело цикла, вы лучше с

do
{
    ...
} while(condition);