while (true) e loop-breaking - anti-pattern?

32

Considera il seguente codice:

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

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Supponiamo che questo processo implichi un numero limitato di passaggi, dipendenti dall'input; il ciclo è progettato per terminare da solo come risultato dell'algoritmo e non è progettato per funzionare indefinitamente (finché non viene annullato da un evento esterno). Poiché il test per vedere se il ciclo deve terminare è nel mezzo di una serie logica di passaggi, il ciclo while non controlla attualmente nulla di significativo; il controllo viene invece eseguito nella posizione "corretta" all'interno dell'algoritmo concettuale.

Mi è stato detto che questo è un codice errato, perché è più soggetto a bug a causa della condizione di fine non controllata dalla struttura del ciclo. È più difficile capire come uscire dal ciclo e potrebbe invitare bug in quanto la condizione di rottura potrebbe essere ignorata o omessa in caso di modifiche future.

Ora il codice potrebbe essere strutturato come segue:

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

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

Tuttavia, questo duplica una chiamata a un metodo nel codice, violando DRY; se TransformInSomeWay è stato successivamente sostituito con qualche altro metodo, entrambe le chiamate dovrebbero essere trovate e modificate (e il fatto che ce ne siano due potrebbe essere meno ovvio in una parte di codice più complessa).

Potresti anche scrivere come:

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

      complete = ProcessingComplete(input);

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

... ma ora disponi di una variabile il cui unico scopo è spostare il controllo delle condizioni nella struttura del ciclo e deve essere controllato più volte per fornire lo stesso comportamento della logica originale.

Per parte mia, dico che dato l'algoritmo che questo codice implementa nel mondo reale, il codice originale è il più leggibile. Se lo stessi facendo tu, questo è il modo in cui ci penserai e quindi sarebbe intuitivo che le persone abbiano familiarità con l'algoritmo.

Quindi, che è "migliore"? è meglio dare la responsabilità del controllo delle condizioni al ciclo while strutturando la logica attorno al ciclo? O è meglio strutturare la logica in un modo "naturale" come indicato dai requisiti o una descrizione concettuale dell'algoritmo, anche se ciò potrebbe significare ignorare le capacità integrate del ciclo?

    
posta KeithS 29.03.2012 - 20:42
fonte

9 risposte

14

Rimani con la tua prima soluzione. I loop possono diventare molto coinvolti e una o più pause pulite possono essere molto più semplici per te, chiunque altro guardi il tuo codice, l'ottimizzatore e le prestazioni del programma piuttosto che elaborare if-statement e variabili aggiunte. Il mio approccio abituale è impostare un ciclo con qualcosa come "while (true)" e ottenere la migliore codifica possibile. Spesso riesco a fare e sostituire le interruzioni con un controllo di ciclo standard; la maggior parte dei loop è piuttosto semplice, dopo tutto. La condizione finale dovrebbe essere controllata dalla struttura del ciclo per i motivi che hai menzionato, ma non al costo dell'aggiunta di nuove variabili, aggiunta di istruzioni if e codice ripetuto, tutti sono anche più inclini ai bug.

    
risposta data 30.03.2012 - 18:04
fonte
13

È solo un problema significativo se il corpo del loop non è banale. Se il corpo è così piccolo, analizzarlo in questo modo è banale e non è affatto un problema.

    
risposta data 29.03.2012 - 21:58
fonte
7

Ho difficoltà a trovare le altre soluzioni meglio di quella con pausa. Bene, preferisco il modo Ada che permette di fare

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

ma la soluzione di interruzione è il rendering più pulito.

Il mio POV è che quando c'è una struttura di controllo mancante, la soluzione più pulita è quella di emularlo con altre strutture di controllo, non usando il flusso di dati. (E allo stesso modo, quando ho un problema di flusso di dati preferisco le soluzioni di flusso di dati puri a quella che coinvolge le strutture di controllo *).

It's more difficult to figure out how you'd exit the loop,

Non essere in errore, la discussione non si verificherebbe se la difficoltà non fosse la stessa: la condizione è la stessa e presente nello stesso luogo.

Quale di

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

e

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

rende meglio la struttura prevista? Sto votando per la prima, non è necessario verificare che le condizioni corrispondano. (Ma vedi la mia indentazione).

    
risposta data 30.03.2012 - 09:19
fonte
3

while (true) e break è un linguaggio comune. È il modo canonico per implementare loop di metà uscita in linguaggi di tipo C. Aggiungere una variabile per memorizzare la condizione di uscita e un if () intorno alla seconda metà del ciclo è un'alternativa ragionevole. Alcuni negozi possono ufficialmente standardizzare su uno o l'altro, ma nessuno dei due è superiore; sono equivalenti.

Un buon programmatore che lavora in queste lingue dovrebbe essere in grado di sapere cosa sta succedendo a colpo d'occhio se lui o lei vede uno dei due. Potrebbe essere una buona domanda per un colloquio; mano a qualcuno due anelli, uno usa ogni idioma, e chiedi loro quale sia la differenza (risposta corretta: "niente", con forse un'aggiunta di "ma io abitualmente uso QUESTO.")

    
risposta data 30.03.2012 - 18:39
fonte
2

Le tue alternative non sono così negative come potresti pensare. Il buon codice dovrebbe:

  1. Indica chiaramente con buoni nomi / commenti di variabili in quali condizioni il ciclo deve essere terminato. (La condizione di rottura non dovrebbe essere nascosta)

  2. Indica chiaramente qual è l'ordine delle operazioni. Qui è dove mettere la terza operazione opzionale ( DoSomethingElseTo(input) ) all'interno di if è una buona idea.

Detto questo, è facile lasciare che il perfezionismo e l'istinto di lucidatura dell'ottone consumino molto tempo. Vorrei solo modificare il se come sopra menzionato; commenta il codice e vai avanti.

    
risposta data 29.03.2012 - 21:55
fonte
2

Le persone dicono che è una pratica sbagliata usare while (true) , e molte volte hanno ragione. Se la tua dichiarazione while contiene un sacco di codice complicato e non è chiaro quando stai uscendo da if, allora ti rimane un codice difficile da mantenere e un semplice bug potrebbe causare un loop infinito.

Nel tuo esempio hai corretto usare while(true) perché il tuo codice è chiaro e facile da capire. Non ha senso creare codice inefficiente e difficile da leggere, semplicemente perché alcune persone considerano sempre una pratica scorretta usare while(true) .

Mi sono trovato di fronte a un problema simile:

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

L'utilizzo di do ... while(true) evita che il isset($array][$key] venga chiamato inutilmente due volte, il codice è più corto, più pulito e più facile da leggere. Non c'è assolutamente alcun rischio che un bug di loop infinito venga introdotto con else break; alla fine.

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

È bene seguire buone pratiche ed evitare cattive pratiche ma ci sono sempre delle eccezioni ed è importante mantenere una mente aperta e realizzare tali eccezioni.

    
risposta data 10.06.2018 - 02:13
fonte
0

Se un particolare flusso di controllo è espresso in modo naturale come un'istruzione break, non è essenzialmente una scelta migliore utilizzare altri meccanismi di controllo del flusso per emulare un'istruzione break.

(nota che questo include parzialmente lo srotolamento del loop e lo scorrimento del corpo del loop in modo che il ciclo inizi coincida con il test condizionale)

Se puoi riorganizzare il tuo loop in modo che il flusso del controllo sia espresso in modo naturale avendo un controllo condizionale all'inizio del ciclo, ottimo. Potrebbe anche valere la pena di spendere un po 'di tempo a pensare se sia possibile. Ma no, non provare ad inserire un piolo quadrato in un buco rotondo.

    
risposta data 25.05.2014 - 19:04
fonte
0

Si potrebbe anche andare con questo:

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

O meno confusamente:

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

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}
    
risposta data 30.03.2012 - 18:55
fonte
0

Quando la complessità della logica diventa ingombrante, l'uso di un loop illimitato con interruzioni mid-loop per il controllo del flusso è in realtà più sensato. Ho un progetto con un codice che assomiglia al seguente. (Notare l'eccezione alla fine del ciclo.)

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();

Per fare questa logica senza interruzioni di loop illimitate, finirei con qualcosa di simile al seguente:

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

Quindi l'eccezione è ora generata in un metodo di supporto. Inoltre ha un bel po 'di if ... else di nidificazione. Non sono soddisfatto di questo approccio. Quindi penso che il primo schema sia il migliore.

    
risposta data 29.04.2018 - 10:49
fonte