Есть ли когда-нибудь необходимость в цикле "do {...} while ()"?


Бьярне Страуструп (создатель C++) однажды сказал, что он избегает циклов "do/while" и предпочитает писать код в терминах цикла "while". [См. цитату ниже.]

услышав это, я обнаружил, что это правда. О чем ты думаешь? Есть ли пример, где "do/while "намного чище и легче понять, чем если бы вы использовали" while " вместо этого?

в ответ на некоторые ответы: да, я понимаю техническую разницу между " do / while" и "пока". Это более глубокий вопрос о читаемости и структурировании кода с использованием циклов.

позвольте мне спросить по-другому: предположим, вам запретили использовать "do / while" - есть ли реалистичный пример, где это не даст вам выбора, кроме как писать нечистый код с помощью"while"?

Из "Языка Программирования C++", 6.3.3:

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

19 63

19 ответов:

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

#include <stdio.h>

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

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

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

другой пример - когда у вас уже есть допустимый объект, когда должна быть запущена первая итерация, поэтому вы не хотите выполните что-либо (включая оценку условий цикла) до начала первой итерации. Пример с функциями Findfirstfile / FindNextFile Win32: вы вызываете FindFirstFile, который либо возвращает ошибку, либо дескриптор поиска в первый файл, а затем вызываете FindNextFile, пока он не вернет ошибку.

псевдокод:

Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
   do {
      process( params ); //process found file
   } while( ( handle = FindNextFile( params ) ) != Error ) );
}

do { ... } while (0) является важной конструкцией для обеспечения хорошего поведения макросов.

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

Edit: я столкнулся с ситуацией, когда do/while был намного чище сегодня в моем собственном коде. Я делал кросс-платформенную абстракцию парного LL / SC инструкция. Они должны использоваться в цикле, как Итак:

do
{
  oldvalue = LL (address);
  newvalue = oldvalue + 1;
} while (!SC (address, newvalue, oldvalue));

(эксперты могут понять, что oldvalue не используется в реализации SC, но он включен, чтобы эту абстракцию можно было эмулировать с помощью CAS.)

LL и SC являются отличным примером ситуации, когда do/while значительно чище, чем эквивалентная форма while:

oldvalue = LL (address);
newvalue = oldvalue + 1;
while (!SC (address, newvalue, oldvalue))
{
  oldvalue = LL (address);
  newvalue = oldvalue + 1;
}

по этой причине я крайне разочарован в том, что Google Go имеет решил удалить do-while construct.

Это полезно, когда вы хотите "сделать" что-то "пока" условие выполняется.

Он может быть сфальсифицирован в цикле while следующим образом:

while(true)
{

    // .... code .....

    if(condition_satisfied) 
        break;
}

(Если вы знаете разницу между обоими)

Do/While хорошо подходит для начальной загрузки / предварительной инициализации кода до проверки вашего состояния и запуска цикла while.

в наших обозначениях

  • если / пока / ... условия не имеют побочных эффектов и
  • переменные должны быть инициализированы.

так у нас почти никогда не бывает do {} while(xx) Потому что:

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

переписывается в:

int main() {
    char c(0);
    while (c < '0' ||  c > '9');  {
        printf("enter a number");
        scanf("%c", &c);
    } 
}

и

Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
   do {
      process( params ); //process found file
   } while( ( handle = FindNextFile( params ) ) != Error ) );
}

переписывается в:

Params params(xxx);
Handle handle = FindFirstFile( params );
while( handle!=Error ) {
    process( params ); //process found file
    handle = FindNextFile( params );
}

следующая общая идиома кажется мне очень простой:

do {
    preliminary_work();
    value = get_value();
} while (not_valid(value));

переписать, чтобы избежать do Кажется:

value = make_invalid_value();
while (not_valid(value)) {
    preliminary_work();
    value = get_value();
}

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

в таких случаях, как эти,do построить очень полезный вариант.

Это все читабельности.
Более читаемый код приводит к меньшей головной боли в обслуживании кода и лучшей совместной работе.
Другие соображения (такие как оптимизация), безусловно, менее важны в большинстве случаев.
Я уточню, так как у меня есть комментарий здесь:
Если у вас есть фрагмент кода A использует do { ... } while(), и это более читабельно, чем его while() {...} эквивалентно B, тогда я бы проголосовал за A. Если вы предпочитаете B, так как вы видите условие цикла "спереди", и вы думаете, что это более читабельным (и таким образом ремонтопригодный, etc.) - тогда вперед, используйте B.
Я хочу сказать: используйте код, который более читаем для ваших глаз (и для ваших коллег). Выбор, конечно, субъективен.

Это только личный выбор, на мой взгляд.

большую часть времени вы можете найти способ переписать do ... цикл while в цикле while, но не всегда. Также может быть более логичным написать цикл do while иногда, чтобы соответствовать контексту, в котором вы находитесь.

Если вы посмотрите выше, ответ от TimW, это говорит само за себя. Второй с ручкой, особенно более грязный, на мой взгляд.

это самая чистая альтернатива, чтобы сделать-в то время как я видел. Это идиома, рекомендуемая для Python, которая не имеет цикла do-while.

один нюанс заключается в том, что вы не можете иметь continue на <setup code> так как он будет прыгать условие разрыва, но ни один из примеров, которые показывают преимущества do-while нужно продолжить перед условием.

while (true) {
       <setup code>
       if (!condition) break;
       <loop body>
}

здесь он применяется к некоторым из лучших примеров циклов do-while выше.

while (true);  {
    printf("enter a number");
    scanf("%c", &c);
    if (!(c < '0' ||  c > '9')) break;
} 

этот следующий пример-это случай, когда структура более читаема, чем do-while, так как условие сохраняется в верхней части как //get data обычно короткий, но //process data часть может быть длительным.

while (true);  {
    // get data 
    if (data == null) break;
    // process data
    // process it some more
    // have a lot of cases etc.
    // wow, we're almost done.
    // oops, just one more thing.
} 

я почти никогда не использую их просто из-за следующего:

несмотря на то, что цикл проверяет состояние post, вам все равно нужно проверить это условие post в своем цикле, чтобы вы не обрабатывали условие post.

возьмите пример псевдокода:

do {
   // get data 
   // process data
} while (data != null);

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

do {
   // get data 
   if (data != null) {
       // process data
   }
} while (data != null);

дополнительная проверка "если" просто не стоит ИМО. Я нашел очень мало случаев, когда это более лаконично, чтобы сделать do-while вместо цикла while. МММ.

в ответ на вопрос/комментарий от неизвестного (google) на ответ Дэна Олсона:

" do { ... } в то время как (0) является важной конструкцией для обеспечения хорошего поведения макросов."

#define M do { doOneThing(); doAnother(); } while (0)
...
if (query) M;
...

вы видите, что происходит без do { ... } while(0)? Он всегда будет выполнять doAnother ().

читать Теорема Структурированной Программы. A do{} while() всегда можно переписать на while () do{}. Последовательность, выбор, итерация и все что нужно.

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

LoopBody()
while(cond) {
    LoopBody()
}

цикл do-while всегда можно переписать как цикл while.

следует ли использовать только while loops, или while, do-while и for-loops (или любую их комбинацию) во многом зависит от вашего вкуса к эстетике и условностям проекта, над которым вы работаете.

лично я предпочитаю while-loops, потому что это упрощает рассуждения об инвариантах цикла IMHO.

а существуют ли ситуации, когда вам нужно сделать-цикл while: вместо из

do
{
  loopBody();
} while (condition());

вы всегда можете

loopBody();
while(condition())
{
  loopBody();
}

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

во-первых, я согласен, что do-while менее читаем, чем while.

но я удивлен, что после стольких ответов никто не подумал, почему do-while даже существует в языке. Причина в эффективности.

допустим, у нас есть do-while петли с N проверка условий, где результат условия зависит от тела цикла. Тогда, если мы заменим его на while петли, мы получаем N+1 условие проверяет вместо этого, где дополнительная проверка бессмысленна. Это не имеет большого значения, если условие цикла содержит только проверку целочисленного значения, но допустим, что у нас есть

something_t* x = NULL;

while( very_slowly_check_if_something_is_done(x) )
{
  set_something(x);
}

тогда вызов функции в первом круге цикла является избыточным: мы уже знаем, что x еще ничего не установлено. Так зачем же выполнять какой-то бессмысленный служебный код?

Я часто использую do-while для этой самой цели при кодировании встроенных систем реального времени, где код внутри условия относительно медленный (проверка ответа от некоторых медленных периферийным оборудованием).

рассмотреть что-то вроде этого:

int SumOfString(char* s)
{
    int res = 0;
    do
    {
        res += *s;
        ++s;
    } while (*s != '');
}

так получилось, что' \0 ' равно 0, но я надеюсь, что вы поняли суть.

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

Если пока был зарезервирован только для пока петли и do/пока был изменен в do/до или повторить/до, Я не думаю, что цикл (что, безусловно, удобно и "правильный" способ кодирования некоторых циклов) вызовет столько проблем.

я уже говорил об этом в отношении JavaScript, который также унаследовал этот жалкий выбор от C.

Ну, может быть, это уходит на несколько шагов, но в случае

do
{
     output("enter a number");
     int x = getInput();

     //do stuff with input
}while(x != 0);

можно было бы, хотя и не обязательно читабельным, чтобы использовать

int x;
while(x = getInput())
{
    //do stuff with input
}

Теперь, если вы хотите использовать число, отличное от 0, чтобы выйти из цикла

while((x = getInput()) != 4)
{
    //do stuff with input
}

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

мне нравится пример Давида Божяка. Однако, чтобы играть в devil's advocate, я чувствую, что вы всегда можете разложить код, который вы хотите запустить хотя бы один раз, на отдельную функцию, достигнув, возможно, наиболее читаемого решения. Например:

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

может стать этот:

int main() {
    char c = askForCharacter();
    while (c < '0' ||  c > '9') {
        c = askForCharacter();
    }
}

char askForCharacter() {
    char c;
    printf("enter a number");
    scanf("%c", &c);
    return c;
}

(простите любой неправильный синтаксис; я не программист C)