Le pratiche di programmazione errate 'interruzione' e 'continua'?

179

Il mio capo continua a menzionare con noncuranza che i programmatori malintenzionati utilizzano break e continue nei cicli.

Li uso sempre perché hanno senso; lascia che ti mostri l'ispirazione:

function verify(object) {
    if (object->value < 0) return false;
    if (object->value > object->max_value) return false;
    if (object->name == "") return false;
    ...
}

Il punto qui è che prima la funzione controlla che le condizioni siano corrette, quindi esegue la funzionalità effettiva. Lo stesso IMO si applica ai loop:

while (primary_condition) {
    if (loop_count > 1000) break;
    if (time_exect > 3600) break;
    if (this->data == "undefined") continue;
    if (this->skip == true) continue;
    ...
}

Penso che sia più facile leggere & di debug; ma non vedo neanche un rovescio della medaglia.

    
posta Mikhail 16.08.2017 - 19:16
fonte

20 risposte

228

Quando vengono usati all'inizio di un blocco, come i primi controlli effettuati, si comportano come precondizioni, quindi va bene.

Se usati nel mezzo del blocco, con un po 'di codice in giro, si comportano come trappole nascoste, quindi è brutto.

    
risposta data 15.03.2011 - 16:08
fonte
83

Potresti leggere il documento del 1974 di Donald Knuth Programmazione strutturata con go alle istruzioni , in cui discute i vari usi del go to che sono strutturalmente desiderabili. Includono l'equivalente delle dichiarazioni break e continue (molti degli usi di go to sono stati sviluppati in costrutti più limitati). Il tuo capo è il tipo per chiamare Knuth un programmatore cattivo?

(Gli esempi dati mi interessano. Tipicamente, break e continue non sono piaciuti alle persone che amano una voce e un'uscita da qualsiasi parte di codice, e quel tipo di persona aggrotta le sopracciglia anche su più% dichiarazioni direturn. )

    
risposta data 15.03.2011 - 16:12
fonte
40

Non credo che siano cattivi L'idea che siano cattivi viene dai tempi della programmazione strutturata. È legato alla nozione che una funzione deve avere un singolo punto di ingresso e un singolo punto di uscita, i. e. solo un return per funzione.

Questo ha un senso se la tua funzione è lunga e se hai più cicli annidati. Tuttavia, le tue funzioni dovrebbero essere brevi e dovresti avvolgere i loop e i loro corpi in brevi funzioni. Generalmente, forzare una funzione ad avere un singolo punto di uscita può generare una logica molto complessa.

Se la tua funzione è molto breve, se hai un loop singolo o, nel peggiore dei casi, due loop annidati e se il corpo del loop è molto corto, allora è molto chiaro che cosa fa break o continue . È anche chiaro quali sono le dichiarazioni multiple return .

Questi problemi sono trattati in "Codice pulito" di Robert C. Martin e in "Refactoring" di Martin Fowler.

    
risposta data 15.03.2011 - 16:11
fonte
32

I programmatori cattivi parlano in modo assoluto (proprio come Sith). I bravi programmatori usano la soluzione più chiara possibile ( a parità di tutte le altre ).

Utilizzare break e continue rende spesso difficile seguire il codice. Ma se la loro sostituzione rende il codice ancora più difficile da seguire, allora è un brutto cambiamento.

L'esempio che hai dato è sicuramente una situazione in cui le interruzioni e continua dovrebbero essere sostituite con qualcosa di più elegante.

    
risposta data 06.10.2011 - 00:34
fonte
22

La maggior parte delle persone pensa che sia una cattiva idea perché il comportamento non è facilmente prevedibile. Se stai leggendo il codice e vedi while(x < 1000){} presumi che verrà eseguito fino a x > = 1000 ... Ma se ci sono interruzioni nel mezzo, allora non è vero, quindi puoi ' t fidati del tuo loop ...

È lo stesso motivo per cui alla gente non piace GOTO: certo, può essere usato bene, ma può anche portare a uno stupefacente codice spaghetti, in cui il codice salta casualmente da una sezione all'altra.

Per quanto mi riguarda, se avessi intenzione di fare un ciclo che si è rotto su più di una condizione, farei while(x){} e quindi commutare da X a false quando ho bisogno di uscire. Il risultato finale sarebbe lo stesso, e chiunque leggendo il codice saprebbe guardare più da vicino le cose che hanno cambiato il valore di X.

    
risposta data 15.03.2011 - 16:07
fonte
14

Sì, è possibile [ri] scrivere programmi senza istruzioni di interruzione (o ritorna dal centro di loop, che fanno la stessa cosa). Ma potrebbe essere necessario introdurre variabili aggiuntive e / o duplicazione del codice che rendono il programma più difficile da capire. Pascal (il linguaggio di programmazione) è stato pessimo soprattutto per i programmatori principianti per questo motivo. Il tuo capo vuole sostanzialmente che tu pianti nelle strutture di controllo di Pascal. Se Linus Torvalds fosse nei tuoi panni, probabilmente mostrerebbe al tuo capo il dito medio!

Esiste un risultato di computer science chiamato gerarchia delle strutture di controllo di Kosaraju, che risale al 1973 e che è menzionata nel (più) famoso documento di Knuth su gotos del 1974. (Questo documento di Knuth era già raccomandato sopra da David Thornley, a proposito.) Ciò che S. Rao Kosaraju ha dimostrato nel 1973 è che non è possibile riscrivere tutti i programmi che hanno interruzioni di profondità multi-livello n in programmi con profondità di interruzione inferiore a n senza introdurre variabili extra. Ma diciamo che è solo un risultato puramente teorico. (Aggiungi solo alcune variabili extra?) Sicuramente puoi farlo per compiacere il tuo capo ...)

Ciò che è molto più importante dal punto di vista dell'ingegneria del software è un più recente documento del 1995 di Eric S. Roberts intitolato Uscite dal ciclo e programmazione strutturata: riapertura del dibattito ( link ). Roberts riassume diversi studi empirici condotti da altri prima di lui. Ad esempio, quando è stato chiesto ad un gruppo di studenti di tipo CS101 di scrivere codice per una funzione che implementa una ricerca sequenziale in un array, l'autore dello studio ha affermato quanto segue su quegli studenti che hanno utilizzato un break / return / goto per uscire da il ciclo di ricerca sequenziale quando l'elemento è stato trovato:

I have yet to find a single person who attempted a program using [this style] who produced an incorrect solution.

Roberts dice anche che:

Students who attempted to solve the problem without using an explicit return from the for loop fared much less well: only seven of the 42 students attempting this strategy managed to generate correct solutions. That figure represents a success rate of less than 20%.

Sì, potresti avere più esperienza degli studenti CS101, ma senza usare l'istruzione break (o equivalentemente return / goto da metà di loop), alla fine scriverò codice che, pur essendo nominativamente ben strutturato, è abbastanza peloso in termini di variabili logiche extra e duplicazione del codice che qualcuno, probabilmente te stesso, inserirà dei bug di logica mentre cerca di seguire lo stile di codifica del tuo capo.

Dirò anche che il lavoro di Roberts è molto più accessibile al programmatore medio, quindi una prima lettura migliore di quella di Knuth. È anche più breve e copre un argomento più ristretto. Probabilmente potresti anche raccomandarlo al tuo capo, anche se è il management piuttosto che il tipo CS.

    
risposta data 15.07.2014 - 01:37
fonte
9

Non considero l'utilizzo di nessuna di queste cattive pratiche, ma utilizzarle troppo all'interno dello stesso ciclo dovrebbe giustificare il ripensamento della logica utilizzata nel ciclo. Usali con parsimonia.

    
risposta data 15.03.2011 - 16:00
fonte
7

L'esempio che hai dato non ha bisogno di pause né continua:

while (primary-condition AND
       loop-count <= 1000 AND
       time-exec <= 3600) {
   when (data != "undefined" AND
           NOT skip)
      do-something-useful;
   }

Il mio 'problema' con le 4 linee nell'esempio è che sono tutte sullo stesso livello ma fanno cose diverse: alcune interrompono, altre continuano ... Devi leggere ogni riga.

Nel mio approccio annidato, più vai a fondo, più diventa "utile" il codice.

Ma, se nel profondo trovassi un motivo per fermare il ciclo (diverso dalle condizioni primarie), un'interruzione o un ritorno avrebbero il suo uso. Preferirei che l'uso di un flag extra che deve essere testato nella condizione di primo livello. L'interruzione / ritorno è più diretto; meglio afferma l'intento che impostare un'altra variabile.

    
risposta data 16.03.2011 - 13:35
fonte
6

La "cattiveria" dipende da come le usi. Generalmente utilizzo le interruzioni nei costrutti di loop SOLO quando mi salverà cicli che non possono essere salvati tramite un refactoring di un algoritmo. Ad esempio, passa da una raccolta alla ricerca di un elemento con un valore in una proprietà specifica impostata su true. Se tutto ciò che devi sapere è che uno degli elementi ha questa proprietà impostata su true, una volta raggiunto tale risultato, un'interruzione è buona per terminare il ciclo in modo appropriato.

Se utilizzare un'interruzione non renderà il codice particolarmente più facile da leggere, più breve da eseguire o salvare cicli nell'elaborazione in modo significativo, allora è meglio non usarli. Io tendo a codificare il "minimo comune denominatore" quando possibile per assicurarmi che chiunque mi segua possa facilmente guardare il mio codice e capire cosa sta succedendo (non ho sempre successo in questo). Le interruzioni lo riducono perché introducono strani punti di entrata / uscita. Abusati possono comportarsi in modo molto simile a una dichiarazione "goto" fuori di testa.

    
risposta data 15.03.2011 - 16:08
fonte
4

Assolutamente no ... Sì, l'uso di goto è sbagliato perché deteriora la struttura del tuo programma e inoltre è molto difficile capire il flusso di controllo.

Tuttavia, l'uso di affermazioni come break e continue è assolutamente necessario in questi giorni e non è affatto considerato come una cattiva pratica di programmazione.

E anche non così difficile da capire il flusso di controllo in uso di break e continue . In costrutti come switch l'istruzione break è assolutamente necessaria.

    
risposta data 27.11.2013 - 15:09
fonte
3

La nozione essenziale viene dall'essere in grado di analizzare semanticamente il tuo programma. Se si dispone di una singola voce e di una singola uscita, la matematica necessaria per indicare gli stati possibili è notevolmente più semplice rispetto alla necessità di gestire i percorsi di biforcazione.

In parte, questa difficoltà si riflette nella capacità di ragionare concettualmente sul tuo codice.

Francamente, il tuo secondo codice non è ovvio. Cosa sta facendo? Continua 'continua', o fa 'avanti' il ciclo? Non ne ho idea. Almeno il tuo primo esempio è chiaro.

    
risposta data 15.03.2011 - 16:56
fonte
3

Sostituirei il tuo secondo snippet di codice con

while (primary_condition && (loop_count <= 1000 && time_exect <= 3600)) {
    if (this->data != "undefined" && this->skip != true) {
        ..
    }
}

non per qualsiasi motivo di scarsa chiarezza - penso che sia davvero più facile da leggere e che qualcuno capisca cosa sta succedendo. In generale, le condizioni per i loop dovrebbero essere contenute puramente all'interno di quelle condizioni di loop non disseminate in tutto il corpo. Tuttavia ci sono alcune situazioni in cui break e continue possono aiutare la leggibilità. break moreso rispetto a continue Potrei aggiungere: D

    
risposta data 13.06.2017 - 23:09
fonte
2

Non sono d'accordo con il tuo capo. Ci sono luoghi appropriati per break e continue da utilizzare. In effetti, il motivo per cui le eccezioni e la gestione delle eccezioni sono state introdotte nei moderni linguaggi di programmazione è che non è possibile risolvere ogni problema utilizzando solo structured techniques .

Nota a margine Non voglio iniziare una discussione religiosa qui, ma potresti ristrutturare il tuo codice per renderlo ancora più leggibile in questo modo:

while (primary_condition) {
    if (loop_count > 1000) || (time_exect > 3600) {
        break;
    } else if ( ( this->data != "undefined") && ( !this->skip ) ) {
       ... // where the real work of the loop happens
    }
}

Su un'altra nota collaterale

Personalmente non mi piace usare ( flag == true ) in condizionali perché se la variabile è già un booleano, allora stai introducendo un confronto addizionale che deve accadere quando il valore del booleano ha la risposta che vuoi - a meno che tu ovviamente sono certo che il tuo compilatore ottimizzerà il confronto extra.

    
risposta data 13.06.2017 - 23:08
fonte
1

Sono d'accordo con il tuo capo. Sono cattivi perché producono metodi con elevata complessità ciclomatica. Tali metodi sono difficili da leggere e difficili da testare. Fortunatamente c'è una soluzione facile. Estrai il corpo del ciclo in un metodo separato, dove "continua" diventa "ritorno". "Return" è migliore perché dopo "return" è finita - non ci sono preoccupazioni per lo stato locale.

Per "break" estrai il ciclo stesso in un metodo separato, sostituendo "break" con "return".

Se i metodi estratti richiedono un numero elevato di argomenti, questa è un'indicazione per estrarre una classe - o raccoglierli in un oggetto di contesto.

    
risposta data 16.08.2017 - 19:28
fonte
0

Penso che sia solo un problema quando annidato profondamente all'interno di più loop. È difficile sapere su quale loop si sta interrompendo. Potrebbe anche essere difficile seguire un continuo, ma penso che il vero dolore derivi dalle interruzioni: la logica può essere difficile da seguire.

    
risposta data 15.03.2011 - 15:54
fonte
0

Fintanto che non vengono usati come goto camuffati nel seguente esempio:

do
{
      if (foo)
      {
             /*** code ***/
             break;
      }

      if (bar)
      {
             /*** code ***/
             break;
      }
} while (0);

Io sto bene con loro. (Esempio visto nel codice di produzione, meh)

    
risposta data 13.06.2017 - 23:09
fonte
0

Non mi piace nessuno di questi stili. Ecco cosa preferirei:

function verify(object)
{
    if not (object->value < 0) 
       and not(object->value > object->max_value)
       and not(object->name == "") 
       {
         do somethign important
       }
    else return false; //probably not necessary since this function doesn't even seem to be defined to return anything...?
}

Non mi piace davvero usare return per interrompere una funzione. Sembra un abuso di return .

Anche l'utilizzo di break non è sempre chiaro.

Ancora meglio potrebbe essere:

notdone := primarycondition    
while (notDone)
{
    if (loop_count > 1000) or (time_exect > 3600)
    {
       notDone := false; 
    }
    else
    { 
        skipCurrentIteration := (this->data == "undefined") or (this->skip == true) 

        if not skipCurrentIteration
        {
           do something
        } 
        ...
    }
}

meno nidificazione e le condizioni complesse sono refactored in variabili (in un programma reale dovresti avere nomi migliori, ovviamente ...)

(Tutto il codice precedente è pseudo-codice)

    
risposta data 13.06.2017 - 23:10
fonte
0

No. È un modo per risolvere un problema e ci sono altri modi per risolverlo.

Molti linguaggi mainstream correnti (Java, .NET (C # + VB), PHP, scrivono i propri) usano "break" e "continue" per saltare i loop. Entrambe le frasi "strutturate goto (s)".

Senza di essi:

String myKey = "mars";

int i = 0; bool found = false;
while ((i < MyList.Count) && (not found)) {
  found = (MyList[i].key == myKey);
  i++;   
}
if (found)
  ShowMessage("Key is " + i.toString());
else
  ShowMessage("Not found.");

Con loro:

String myKey = "mars";

for (i = 0; i < MyList.Count; i++) {
  if (MyList[i].key == myKey)
    break;
}
ShowMessage("Key is " + i.toString());

Nota che il codice "break" e "continue" è più breve e di solito trasforma le frasi "while" in "for" o "foreach".

Entrambi i casi sono una questione di stile di codifica. Preferisco non usarli , perché lo stile dettagliato consente di avere un maggiore controllo del codice.

In realtà, lavoro in alcuni progetti, dove era obbligatorio usare quelle frasi.

Alcuni sviluppatori potrebbero pensare che non siano necesari, ma ipotetici, se dovessimo rimuoverli, dobbiamo rimuovere "while" e "do while" ("repeat until", you pascal guys) anche; -)

Conclusioni, anche se preferisco non usarle, penso che sia un'opzione, non una cattiva pratica di programmazione.

    
risposta data 13.06.2017 - 23:10
fonte
0

Non sono contro continue e break in linea di principio, ma penso che siano costrutti di livello molto basso che molto spesso possono essere sostituiti da qualcosa di ancora migliore.

Sto usando C # come esempio qui, considera il caso di voler iterare su una raccolta, ma vogliamo solo gli elementi che soddisfano qualche predicato, e non vogliamo fare più di un massimo di 100 iterazioni.

for (var i = 0; i < collection.Count; i++)
{
    if (!Predicate(item)) continue;
    if (i >= 100) break; // at first I used a > here which is a bug. another good thing about the more declarative style!

    DoStuff(item);
}

Questo sembra pulito in modo REASONABILE. Non è molto difficile da capire. Penso che trarrebbe molto dall'essere più dichiarativo. Confrontalo con il seguente:

foreach (var item in collection.Where(Predicate).Take(100))
    DoStuff(item);

Forse le chiamate Where and Take non dovrebbero nemmeno essere in questo metodo. Forse questo filtraggio dovrebbe essere fatto PRIMA che la collezione sia passata a questo metodo. Ad ogni modo, allontanandosi dagli elementi di basso livello e concentrandosi maggiormente sulla logica di business effettiva, diventa più chiaro ciò a cui siamo ATTUALMENTE interessati. Diventa più facile separare il nostro codice in moduli coesivi che aderiscono maggiormente alle buone pratiche di progettazione e così on.

Le cose di basso livello continueranno a esistere in alcune parti del codice, ma vogliamo nasconderlo il più possibile, perché ci vuole energia mentale che potremmo usare per ragionare sui problemi di business.

    
risposta data 13.06.2017 - 23:11
fonte
-1

Codice completo ha una bella sezione sull'utilizzo di goto e più ritorni dalla routine o dal ciclo.

In generale non è una cattiva pratica. break o continue indica esattamente cosa succede dopo. E sono d'accordo con questo.

Steve McConnell (autore di Code Complete) utilizza quasi gli stessi esempi per mostrare i vantaggi dell'utilizzo di varie dichiarazioni goto .

Tuttavia l'uso eccessivo di break o continue potrebbe portare a software complesso e non più gestibile.

    
risposta data 16.03.2011 - 16:34
fonte