в той час, як (правда) і розбиття циклу - антидіаграма?


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, структуруючи логіку навколо циклу? Або краще структурувати логіку "природним" способом, як це вказується вимогами чи концептуальним описом алгоритму, навіть якщо це може означати обхід вбудованих можливостей циклу?


12
Можливо, але, незважаючи на зразки коду, питання, що задається, є ІМО більш концептуальним, що більше відповідає цій області ПП. Що таке візерунок чи анти-шаблон, тут обговорюють часто, і це справжнє питання; структурує цикл для запуску нескінченно, а потім погано вирветься з нього?
KeithS

3
do ... untilКонструкція також корисна для цих видів петель , так як вони не повинні бути «заґрунтувати» .
Blrfl

2
У справі з циклом і половиною все ще відсутня справді хороша відповідь.
Лорен Печтел

1
"Однак це дублює виклик методу в коді, порушуючи DRY" Я не згоден з цим твердженням, і це здається нерозумінням принципу.
Енді,

1
Окрім того, що я віддаю перевагу C-стилю для (;;), що явно означає "У мене є цикл, і я не перевіряю в заяві циклу", ваш перший підхід абсолютно правильний. У вас є петля. Перед тим, як вирішити, вийти, потрібно виконати певну роботу. Потім ви виходите, якщо виконуються деякі умови. Тоді ви робите фактичну роботу і починаєте все спочатку. Це абсолютно чудово, легко зрозуміти, логічно, не надмірно та легко вийти правильно. Другий варіант просто жахливий, тоді як третій - просто безглуздий; перетворюється на жахливий, якщо решта петлі, яка переходить у, якщо дуже довга.
gnasher729

Відповіді:


14

Дотримуйтесь свого першого рішення. Цикли можуть стати дуже задіяними, і один чи більше чистих перерв можуть бути набагато простішими для вас, будь-хто інший, хто дивиться на ваш код, оптимізатор та продуктивність програми, ніж детальні оператори if-додавання та додані змінні. Мій звичайний підхід - налаштувати цикл з чимось на зразок "while (true)" і отримати найкраще кодування, яке я можу. Часто я можу і тоді замінити перерви (и) на стандартне керування циклом; Зрештою, більшість петель досить прості. Умови закінчення повинні перевірятися структурою циклу з причин, які ви згадуєте, але не ціною додавання цілих нових змінних, додавання if-операторів та повторення коду, все це ще більше схильне до помилок.


"if-заяви та повторюваний код, усі вони ще більше схильні до помилок." - Так, при спробах людей, яких я називаю, якщо це "майбутні помилки"
Пат

13

Це лише змістовна проблема, якщо тіло циклу нетривіальне. Якщо організм настільки малий, то аналізувати його, як це, є тривіальним, і зовсім не проблема.


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
    }
}

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


1
О, дивіться, мова, яка безпосередньо підтримує петлі середнього виходу! :)
mjfgates

Мені подобається ваша думка щодо емуляції відсутніх контрольних структур керуючими структурами. Це, мабуть, хороша відповідь і на питання, пов'язані з GOTO. Слід використовувати мовні структури управління, коли вони відповідають семантичним вимогам, але якщо алгоритм вимагає чогось іншого, слід використовувати структуру управління, необхідну алгоритму.
supercat

3

в той час як (правда) і перерва - це загальна ідіома. Це канонічний спосіб реалізації циклів середнього виходу на мовах, подібних С. Додавання змінної для збереження умови виходу та if () навколо другої половини циклу є розумною альтернативою. Деякі магазини можуть офіційно стандартизувати ту чи іншу, але жоден не є вищим; вони рівнозначні.

Хороший програміст, що працює на цих мовах, повинен мати можливість знати з першого погляду, якщо він чи вона бачить одну з них. Це може зробити гарне невелике запитання про інтерв'ю; передайте комусь дві петлі, одна використовуючи кожну ідіому, і запитайте у них, в чому різниця (правильна відповідь: "нічого", можливо, додавання ", але я звично використовую ТОМУ")


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гніздування. Я не задоволений таким підходом. Тому я думаю, що перший зразок найкращий.


Дійсно, я задав це питання на SO і був збитий полум'ям ... stackoverflow.com/questions/47253486/…
intrepidis
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.