В то время как (правда) и разрыв цикла - анти-шаблон?

32

Рассмотрим следующий код:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Предположим, что этот процесс включает в себя конечное, но зависящее от ввода число шагов; Цикл предназначен для самостоятельного завершения в результате работы алгоритма и не рассчитан на бесконечное выполнение (до тех пор, пока не будет отменено внешним событием). Поскольку проверка того, должен ли цикл завершиться, находится в середине логического набора шагов, сам цикл while в настоящее время не проверяет ничего значащего; вместо этого проверка выполняется в «правильном» месте концептуального алгоритма.

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

Теперь код можно структурировать следующим образом:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

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

Вы также можете написать это так:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

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

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

Так что же лучше? лучше ли возложить ответственность за проверку условий на цикл while, структурируя логику вокруг цикла? Или лучше структурировать логику «естественным» образом, как указано требованиями или концептуальным описанием алгоритма, даже если это может означать обход встроенных возможностей цикла?

Keiths
источник
12
Возможно, но, несмотря на примеры кода, вопрос, который задают, является IMO более концептуальным, что больше соответствует этой области SE. Что такое паттерн или антишаблон обсуждается здесь часто, и это реальный вопрос; Является ли структурирование цикла для бесконечного запуска и последующего выхода из него плохой вещью?
KeithS
3
do ... untilКонструкция также полезна для этих видов петель , так как они не должны быть «загрунтовать» .
Blrfl
2
В этом случае все еще отсутствует какой-либо действительно хороший ответ.
Лорен Печтел
1
«Однако это дублирует вызов метода в коде, нарушая DRY». Я не согласен с этим утверждением, и это кажется неправильным пониманием принципа.
Энди
1
Помимо того, что я предпочитаю стиль C для (;;), который явно означает «у меня есть цикл, и я не проверяю оператор цикла», ваш первый подход абсолютно верен. У вас есть петля. Есть некоторая работа, прежде чем вы сможете решить, выходить ли. Затем вы выходите, если выполнены некоторые условия. Затем вы выполняете реальную работу и начинаете все сначала. Это абсолютно нормально, легко для понимания, логично, не избыточно, и легко понять правильно. Второй вариант просто ужасен, а третий просто бессмысленен; превращается в ужасный, если остаток цикла, который входит в if, очень длинный.
gnasher729

Ответы:

14

Придерживайтесь своего первого решения. Циклы могут стать очень сложными, и один или несколько чистых разрывов могут быть намного проще для вас, для любого, кто смотрит на ваш код, оптимизатор и производительность программы, чем для сложных операторов if и добавленных переменных. Мой обычный подход - создать цикл с чем-то вроде «while (true)» и получить лучшее кодирование, какое только можно. Часто я могу и действительно заменяю разрыв (ы) стандартным циклическим управлением; В конце концов, большинство петель довольно просты. Конечное условие должно проверяться структурой цикла по указанным вами причинам, но не за счет добавления целых новых переменных, добавления операторов if и повторяющегося кода, которые все еще более подвержены ошибкам.

RalphChapin
источник
«операторы if и повторяющийся код, все из которых еще более подвержены ошибкам». - да, когда пытаюсь людей, которых я называю «будущими ошибками»,
Пэт
13

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

DeadMG
источник
7

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

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

но решение разрыва - самый чистый рендеринг.

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

Сложнее понять, как выйти из цикла,

Не заблуждайтесь, обсуждение не состоялось бы, если бы сложность не была в основном одинаковой: состояние одинаковое и присутствует в одном и том же месте.

Какой из

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

а также

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

сделать лучше предполагаемую структуру? Я голосую за первое, вам не нужно проверять соответствие условий. (Но посмотрите мой отступ).

AProgrammer
источник
1
О, смотрите, язык, который напрямую поддерживает циклы среднего выхода! :)
mjfgates
Мне нравится ваша точка зрения об эмуляции отсутствующих структур управления со структурами управления. Это, вероятно, хороший ответ на вопросы, связанные с GOTO. Следует использовать структуры управления языка всякий раз, когда они соответствуют семантическим требованиям, но если алгоритм требует чего-то другого, следует использовать структуру управления, требуемую алгоритмом.
суперкат
3

while (true) и break - распространенная идиома. Это канонический способ реализации циклов среднего выхода в C-подобных языках. Добавление переменной для хранения условия выхода и if () вокруг второй половины цикла является разумной альтернативой. Некоторые магазины могут официально стандартизировать один или другой, но ни один из них не превосходит; они эквивалентны.

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

mjfgates
источник
2

Ваши альтернативы не так плохи, как вы думаете. Хороший код должен:

  1. Четко укажите с хорошими именами переменных / комментариями, при каких условиях цикл должен быть прекращен. (Условие разрушения не должно быть скрыто)

  2. Четко укажите, каков порядок выполнения операций. Это где дополнительная 3-я операция ( DoSomethingElseTo(input)) внутри if является хорошей идеей.

Тем не менее, легко позволить перфекционистским инстинктам полировки латуни отнимать много времени. Я бы просто настроил, если, как упоминалось выше; закомментируйте код и двигайтесь дальше.

похлопывание
источник
Я думаю, что перфекционист, медные инстинкты полировки экономят время в долгосрочной перспективе. Надеюсь, с практикой вы выработаете такие привычки, чтобы ваш код был идеальным, а его отладка без проблем отнимала много времени. Но в отличие от физической конструкции, программное обеспечение может работать вечно и использоваться в бесконечном количестве мест. Экономия от кода, который даже на 0,00000001% быстрее, надежнее, удобнее в использовании и обслуживании, может быть значительной. (Конечно, большинство моих много полировкой код на языках давно устаревших или зарабатывать деньги для кого - то в эти дни, но это было помочь мне развить хорошие привычки.)
RalphChapin
Ну , я не могу назвать тебя неправильно :-) Это является вопросом мнения и если хорошие навыки вышли из латуни полировки: большой.
Пэт
Я думаю, что это возможность рассмотреть, в любом случае. Я, конечно, не хотел бы предлагать какие-либо гарантии. Мне действительно легче думать, что моя шлифовка из латуни улучшила мои навыки, поэтому я могу быть предубежден в этом вопросе.
RalphChapin
2

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

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

Я столкнулся с похожей проблемой:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

Использование do ... while(true)позволяет избежать isset($array][$key]ненужного повторного вызова, код становится короче, чище и проще для чтения. Нет абсолютно никакого риска появления ошибки бесконечного цикла else break;в конце.

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

Хорошо следовать хорошей практике и избегать плохой практики, но всегда есть исключения, и важно сохранять непредвзятость и осознавать эти исключения.

Дэн Брей
источник
0

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

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

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


источник
0

Вы также можете пойти с этим:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

Или менее запутанно:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}
Затмение
источник
1
В то время как некоторым это нравится, я утверждаю, что условие цикла обычно должно быть зарезервировано для условных тестов, а не для операторов, которые делают вещи.
5
Выражение запятой в условии цикла ... Это ужасно. Ты слишком умный. И это работает только в этом тривиальном случае, когда TransformInSomeWay может быть выражен как выражение.
gnasher729
0

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

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

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

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

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

intrepidis
источник
В самом деле, я задал этот вопрос на SO и был застрелен в огне ... stackoverflow.com/questions/47253486/…
intrepidis