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 comporti un numero finito di passaggi ma dipendenti dall'input; il loop è progettato per terminare da solo come risultato dell'algoritmo e non è progettato per funzionare indefinitamente (fino a quando non viene annullato da un evento esterno). Poiché il test per vedere se il ciclo dovrebbe terminare è nel mezzo di una serie logica di passaggi, il ciclo while stesso attualmente non controlla nulla di significativo; il controllo viene invece eseguito nel punto "corretto" all'interno dell'algoritmo concettuale.

Mi è stato detto che questo è un codice errato, perché è più soggetto a bug a causa della condizione finale non controllata dalla struttura del ciclo. È più difficile capire come usciresti dal ciclo e potresti invitare bug in quanto la condizione di rottura potrebbe essere aggirata o omessa per errore a causa di cambiamenti futuri.

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 in TransformInSomeWayseguito venissero sostituiti 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 un pezzo di codice più complesso).

Puoi anche scriverlo come:

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

      complete = ProcessingComplete(input);

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

... ma ora hai una variabile il cui unico scopo è spostare il controllo delle condizioni sulla struttura del loop e deve anche essere verificato più volte per fornire lo stesso comportamento della logica originale.

Da parte mia, dico che dato l'algoritmo che questo codice implementa nel mondo reale, il codice originale è il più leggibile. Se lo stessi affrontando da solo, questo è il modo in cui ci penseresti, quindi sarebbe intuitivo per le persone che hanno familiarità con l'algoritmo.

Quindi, qual è il "migliore"? è meglio dare la responsabilità del controllo delle condizioni al ciclo while strutturando la logica attorno al ciclo? Oppure è meglio strutturare la logica in modo "naturale" come indicato dai requisiti o da una descrizione concettuale dell'algoritmo, anche se ciò può significare bypassare le funzionalità integrate del loop?


12
Forse, ma nonostante gli esempi di codice, la domanda che viene posta è l'IMO più concettuale, il che è più in linea con quest'area della SE. Che cos'è un modello o un anti-modello è discusso qui spesso, e questa è la vera domanda; strutturare un circuito per correre all'infinito e poi espellerlo è una cosa negativa?
KeithS

3
Il do ... untilcostrutto è utile anche per questo tipo di loop poiché non devono essere "innescati".
Blrfl

2
Nel caso del loop e mezzo manca ancora una risposta davvero valida.
Loren Pechtel,

1
"Tuttavia, questo duplica una chiamata a un metodo in codice, violando DRY" Non sono d'accordo con tale affermazione e sembra un equivoco del principio.
Andy,

1
A parte me che preferisco lo stile C per (;;) che significa esplicitamente "Ho un ciclo e non controllo nell'istruzione ciclo", il tuo primo approccio è assolutamente giusto. Hai un ciclo. C'è del lavoro da fare prima che tu possa decidere se uscire. Quindi esci se vengono soddisfatte alcune condizioni. Quindi fai il vero lavoro e ricomincia da capo. Va benissimo, facile da capire, logico, non ridondante e facile da ottenere. La seconda variante è semplicemente orribile, mentre la terza è semplicemente inutile; trasformandosi in terribile se il resto del ciclo che entra nell'if è molto lungo.
gnasher729,

Risposte:


14

Resta con la tua prima soluzione. I loop possono essere molto coinvolti e una o più pause pulite possono essere molto più facili per te, chiunque guardi il tuo codice, l'ottimizzatore e le prestazioni del programma rispetto a elaborate istruzioni if ​​e variabili aggiunte. Il mio approccio abituale è creare un loop con qualcosa come "while (true)" e ottenere il miglior codice possibile. Spesso posso e poi sostituire le interruzioni con un controllo ad anello standard; la maggior parte dei loop è piuttosto semplice, dopo tutto. La condizione finale dovrebbe essere verificata dalla struttura del ciclo per i motivi menzionati, ma non a scapito dell'aggiunta di variabili completamente nuove, dell'aggiunta di istruzioni if ​​e del codice ripetuto, che sono ancora più soggetti a bug.


"istruzioni if ​​e codice ripetuto, che sono ancora più soggetti a bug." - Sì, quando provo le persone che chiamo if sono "bug futuri"
Pat

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.


7

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

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

ma la soluzione break è il rendering più pulito.

Il mio POV è che quando manca una struttura di controllo, la soluzione più pulita è emularla con altre strutture di controllo, non usando il flusso di dati. (E allo stesso modo, quando ho un problema di flusso di dati per preferire soluzioni di puro flusso di dati a quella che coinvolge le strutture di controllo *).

È più difficile capire come uscire dal ciclo,

Non confondere, la discussione non avrebbe luogo se la difficoltà non fosse sostanzialmente la stessa: la condizione è la stessa e presente nello stesso posto.

Quale di

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

e

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

rendere migliore la struttura prevista? Sto votando per il primo, non è necessario verificare che le condizioni corrispondano. (Ma vedi il mio rientro).


1
Oh, guarda, un linguaggio che non supporta direttamente i loop metà uscita! :)
mjfgates

Mi piace il tuo punto sull'emulazione di strutture di controllo mancanti con strutture di controllo. Questa è probabilmente una buona risposta anche alle domande relative a GOTO. Si dovrebbero usare le strutture di controllo di un linguaggio ogni volta che soddisfano i requisiti semantici, ma se un algoritmo richiede qualcos'altro si dovrebbe usare la struttura di controllo richiesta dall'algoritmo.
supercat,

3

while (true) e break è un linguaggio comune. È il modo canonico di implementare loop a metà uscita in linguaggi di tipo C. L'aggiunta di una variabile per memorizzare la condizione di uscita e un if () attorno alla seconda metà del ciclo è un'alternativa ragionevole. Alcuni negozi possono standardizzare ufficialmente l'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 vede uno dei due. Potrebbe fare una buona piccola intervista; mano a qualcuno due anelli, uno usando ogni linguaggio, e chiedi loro qual è la differenza (risposta corretta: "niente", con forse un'aggiunta di "ma io uso abitualmente QUELLO").


2

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

  1. Indicare chiaramente con nomi / commenti variabili validi a quali condizioni il ciclo dovrebbe essere chiuso. (La condizione di rottura non deve essere nascosta)

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

Detto questo, è facile lasciare che l'istinto perfezionista e lucidatore di ottone impieghi molto tempo. Vorrei solo modificare il se come menzionato sopra; commentare il codice e andare avanti.


Penso che l'istinto perfezionista e di lucidatura degli ottoni faccia risparmiare tempo nel lungo periodo. Spero che, con la pratica, sviluppi tali abitudini in modo che il tuo codice sia perfetto e il suo ottone lucido senza perdere molto tempo. Ma a differenza della costruzione fisica, il software può durare per sempre ed essere utilizzato in un numero infinito di posti. I risparmi dal codice che sono persino dello 0,00000001% più veloci, più affidabili, utilizzabili e gestibili possono essere notevoli. (Certo, la maggior parte del mio codice molto raffinato è in lingue ormai obsolete o che fanno soldi per qualcun altro in questi giorni, ma mi ha aiutato a sviluppare buone abitudini.)
RalphChapin

Beh, non posso chiamarti sbagliato :-) Questa è una domanda d'opinione e se dalla lucidatura degli ottoni sono emerse buone abilità: grandioso.
Pat

Penso che sia una possibilità da considerare, comunque. Non vorrei certo offrire alcuna garanzia. Mi fa sentire meglio pensare che la mia lucidatura dell'ottone abbia migliorato le mie capacità, quindi potrei essere prevenuto su questo punto.
RalphChapin

2

La gente dice che è una cattiva pratica da usare while (true), e molte volte hanno ragione. Se la tua whiledichiarazione contiene un sacco di codice complicato ed è poco chiaro quando stai uscendo da if, allora ti rimane un codice difficile da mantenere e un semplice bug potrebbe causare un ciclo infinito.

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

Ho dovuto affrontare 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'uso do ... while(true)evita isset($array][$key]inutilmente di essere chiamato due volte, il codice è più breve, più pulito e più facile da leggere. Non vi è assolutamente alcun rischio che else break;alla fine venga introdotto un bug loop infinito .

$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 le buone pratiche ed evitare le cattive pratiche, ma ci sono sempre delle eccezioni ed è importante mantenere una mente aperta e realizzare quelle eccezioni.


0

Se un determinato flusso di controllo viene naturalmente espresso come un'istruzione break, essenzialmente non è mai una scelta migliore utilizzare altri meccanismi di controllo del flusso per emulare un'istruzione break.

(nota che ciò include lo srotolamento parziale del loop e lo scorrimento del corpo del loop verso il basso in modo che il loop inizi coincida con il test condizionale)

Se riesci a riorganizzare il tuo loop in modo che il flusso di controllo sia naturalmente espresso avendo un controllo condizionale all'inizio del loop, ottimo. Potrebbe anche valere la pena di dedicare un po 'di tempo a pensare se sia possibile. Ma non farlo, non cercare di inserire un piolo quadrato in un foro rotondo.


0

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

1
Mentre ad alcune persone piace, asserisco che la condizione del ciclo dovrebbe essere generalmente riservata ai test condizionali, piuttosto che alle dichiarazioni che fanno cose.

5
Espressione virgola in una condizione di ciclo ... È terribile. Sei troppo intelligente. E funziona solo in questo banale caso in cui TransformInSomeWay può essere espresso come espressione.
gnasher729,

0

Quando la complessità della logica diventa ingombrante, l'utilizzo di un loop illimitato con interruzioni a loop intermedio per il controllo del flusso ha effettivamente più senso. 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 interruzione illimitata del loop, 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 viene ora generata in un metodo di supporto. Inoltre ha parecchioif ... else nidificazione. Non sono soddisfatto di questo approccio. Pertanto penso che il primo modello sia il migliore.


In effetti, ho posto questa domanda su SO e sono stato abbattuto dalle fiamme ... stackoverflow.com/questions/47253486/…
intrepidis
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.