All'interno di un ciclo for, dovrei spostare la condizione di interruzione nel campo delle condizioni, se possibile? [chiuso]

48

A volte ho bisogno di cicli che richiedono un'interruzione del genere:

for(int i=0;i<array.length;i++){
    //some other code
    if(condition){
        break;
    }
}

Mi sento a disagio con la scrittura

if(condition){
    break;
}

perché consuma 3 righe di codice. E ho trovato che il loop può essere riscritto come:

                                   ↓
for(int i=0;i<array.length && !condition;i++){
    //some other code
}

Quindi la mia domanda è, è buona pratica spostare la condizione nel campo delle condizioni per ridurre le linee di codici, se possibile?

    
posta mmmaaa 27.02.2018 - 10:27
fonte

16 risposte

154

Questi due esempi che hai dato non sono funzionalmente equivalenti. Nell'originale, il test delle condizioni viene eseguito dopo la sezione "some other code", mentre nella versione modificata, viene eseguita per prima, all'inizio del corpo del loop.

Il codice non dovrebbe mai essere riscritto con l'unico scopo di ridurre il numero di righe. Ovviamente è un bel bonus quando funziona in quel modo, ma non dovrebbe mai essere fatto a scapito della leggibilità o della correttezza.

    
risposta data 27.02.2018 - 10:39
fonte
51

Non compro l'argomento che "consuma 3 righe di codice" e quindi è cattivo. Dopo tutto, puoi semplicemente scriverlo come:

if (condition) break;

e consuma solo una riga.

Se if appare a metà del ciclo, deve ovviamente esistere come test separato. Tuttavia, se questo test viene visualizzato alla fine del blocco, è stato creato un ciclo che ha un test continua ad entrambe le estremità del ciclo, eventualmente aggiungendo alla complessità del codice. Sappi però che a volte il test if (condition) può avere senso solo dopo che il blocco è stato eseguito.

Ma supponendo che non sia così, adottando l'altro approccio:

for(int i=0;i<array.length && !condition;i++){
    //some other code
}

le condizioni di uscita vengono mantenute insieme, il che può semplificare le cose (specialmente se " qualche altro codice " è lungo).

Ovviamente non c'è una risposta giusta. Quindi, sii consapevole degli idiomi della lingua, delle convenzioni di codifica della tua squadra, ecc. Ma a conti fatti, adotterei l'approccio del test singolo quando ha senso dal punto di vista funzionale.

    
risposta data 27.02.2018 - 10:42
fonte
48

Questo tipo di domande ha scatenato il dibattito quasi finché la programmazione è andata avanti. Per lanciare il mio cappello sul ring, sceglierei la versione con la condizione break ed ecco perché (per questo caso specifico ):

Il ciclo for è solo lì per specificare i valori iterating nel ciclo. Codificare la condizione di rottura all'interno di ciò equivale a confondere la logica IMO.

Avere un break separato rende chiaro che durante la normale elaborazione, i valori sono come specificati nel ciclo for e l'elaborazione dovrebbe continuare tranne dove arriva la condizione.

Come sfondo, ho iniziato a codificare un break , quindi ho inserito la condizione nel ciclo for per anni (sotto la direzione di un programmatore eccezionale) e poi sono tornato allo stile break perché è sembrato molto più pulito (ed era lo stile prevalente nei vari tomi di codice che stavo consumando).

È un'altra di quelle guerre sante alla fine della giornata - alcuni dicono che dovresti never uscire da un loop artificialmente, altrimenti non è un loop reale. Fai ciò che senti sia leggibile (sia per te stesso che per gli altri). Se la riduzione delle sequenze di tasti è davvero la tua passione, vai a giocare a golf nel weekend - birra facoltativa.

    
risposta data 27.02.2018 - 12:23
fonte
43

for loop sono per l'iterazione per qualcosa 1 - non sono solo lol-let's-pull-some-random-stuff-from-the-body -e-put-them-in-the-loop-header - le tre espressioni hanno significati molto specifici:

  • Il primo è per inizializzare l' iteratore . Può essere un indice, un puntatore, un oggetto di iterazione o qualsiasi altra cosa, purché sia usato per iterazione .
  • Il secondo è per verificare se siamo arrivati alla fine.
  • Il terzo è per far avanzare l'iteratore.

L'idea è di separare la gestione dell'iterazione ( come per iterare) dalla logica all'interno del ciclo ( cosa fare in ogni iterazione). Molte lingue di solito hanno un ciclo for-each che ti libera dai dettagli dell'iterazione, ma anche se la tua lingua non ha quel costrutto, o se non può essere usato nel tuo scenario, dovresti comunque limitare l'intestazione del ciclo alla gestione dell'iterazione.

Quindi, devi chiederti: la tua condizione sulla gestione dell'iterazione o sulla logica all'interno del ciclo? È probabile che riguardi la logica all'interno del ciclo, quindi è necessario verificarlo all'interno del ciclo piuttosto che nell'intestazione.

1 Contrariamente agli altri anelli, che "aspettano" che qualcosa accada. Un ciclo for / foreach dovrebbe avere il concetto di una "lista" di elementi da iterare - anche se quella lista è pigra o infinita o astratta.

    
risposta data 27.02.2018 - 12:02
fonte
8

Raccomando di utilizzare la versione con if (condition) . È più leggibile e più facile da eseguire il debug. Scrivere 3 linee extra di codice non ti spezzerà le dita, ma renderà più facile per la prossima persona capire il tuo codice.

    
risposta data 27.02.2018 - 10:33
fonte
8

Se stai iterando su una raccolta in cui alcuni elementi dovrebbero essere saltati, allora ti consiglio una nuova opzione:

  • Filtra la raccolta prima di iterare

Sia Java che C # rendono questo compito relativamente banale. Non sarei sorpreso se C ++ avesse un modo di fare un iteratore di fantasia per saltare determinati elementi che non si applicano. Ma questa è solo un'opzione reale se la tua lingua preferita lo supporta, e il motivo per cui stai usando break è perché ci sono delle condizioni sul fatto che tu elabori un elemento o meno.

Altrimenti c'è una buona ragione per rendere la tua condizione una parte del ciclo for - assumendo che la sua valutazione iniziale sia corretta.

  • È più facile vedere quando un ciclo finisce presto

Ho lavorato su codice in cui il ciclo for richiede poche centinaia di righe con diversi condizionali e alcuni complessi calcoli in corso. I problemi che hai incontrato sono:

  • I comandi break nascosti nella logica sono difficili da trovare e talvolta sorprendenti
  • Il debug richiede di passare attraverso ogni iterazione del ciclo per capire veramente cosa sta succedendo
  • Gran parte del codice non ha refactoring facilmente reversibili o test unitari per aiutare con l'equivalenza funzionale dopo una riscrittura

In questa situazione, consiglio le seguenti linee guida in ordine di preferenza:

  • Filtra la raccolta per eliminare la necessità di un break se possibile
  • Crea la parte condizionale delle condizioni del ciclo for
  • Posiziona i condizionali con break all'inizio del ciclo se possibile
  • Aggiungi un commento su più righe attirando l'attenzione sull'interruzione e sul motivo per cui è presente

Queste linee guida sono sempre soggette alla correttezza del tuo codice. Scegli l'opzione che può rappresentare al meglio l'intento e migliorare la chiarezza del tuo codice.

    
risposta data 27.02.2018 - 14:43
fonte
8

Consiglio vivamente di adottare l'approccio di least surprise a meno che tu non ottenga un vantaggio significativo facendo diversamente.

Le persone non leggono ogni lettera quando leggono una parola e non leggono ogni parola quando leggono un libro - se sono abili a leggere, guardano i contorni di parole e frasi e lasciano che il loro cervello si riempia nel resto.

Quindi è probabile che lo sviluppatore occasionale ritenga che questo sia solo uno standard per il ciclo e nemmeno guardarlo:

for(int i=0;i<array.length&&!condition;i++)

Se vuoi usare quello stile a prescindere, ti consiglio di cambiare le parti for(int i=0;i< e ;i++) che dicono al cervello del lettore che questo è uno standard per il ciclo.

Un altro motivo per andare con if - break è che non puoi sempre usare il tuo approccio con la for -condition. Devi usare if - break quando la condizione di interruzione è troppo complessa per nascondersi all'interno di un'istruzione for , o fa affidamento su variabili accessibili solo all'interno del ciclo for .

for(int i=0;i<array.length&&!((someWidth.X==17 && y < someWidth.x/2) || (y == someWidth.x/2 && someWidth.x == 18);i++)

Quindi, se decidi di andare con if - break , sei coperto. Ma se decidi di andare con for -conditions, devi usare un mix di if - break e for - condizioni. Per essere coerenti, dovrai spostare le condizioni avanti e indietro tra le condizioni for e if - break ogni volta che le condizioni cambiano.

    
risposta data 28.02.2018 - 02:09
fonte
4

La tua trasformazione suppone che qualunque cosa condition venga valutata a true mentre entri nel ciclo. Non possiamo commentare la correttezza di ciò in questo caso perché non è definito.

Avendo successo nel "ridurre le linee di codice" qui, puoi quindi andare a guardare

for(int i=0;i<array.length;i++){
    // some other code
    if(condition){
        break;
    }
    // further code
}

e applica la stessa trasformazione, arrivando a

for(int i=0;i<array.length && !condition;i++){
    // some other code
    // further code
}

Che è una trasformazione non valida, dato che ora condividi incondizionatamente further code dove in precedenza non eri.

Uno schema di trasformazione molto più sicuro consiste nell'estrarre some other code e nella valutazione di condition in una funzione separata

bool evaluate_condition(int i) // This is an horrible name, but I have no context to improve it
{
     // some other code
     return condition;
}

for(int i=0;i<array.length && !evaluate_condition(i);i++){
    // further code
}
    
risposta data 27.02.2018 - 12:50
fonte
4

TL; DR Fai ciò che legge meglio in una determinata circostanza

Prendiamo questo esempio:

for ( int i = 1; i < array.length; i++ ) 
{
    if(something) break;
    // perform operations
}

In questo caso non vogliamo che il codice del ciclo for venga eseguito se something è true, quindi spostare il test di something nel campo delle condizioni è ragionevole.

for ( int i = 1; i < array.length && !something; i++ )

Poi di nuovo, a seconda se something può essere impostato su true prima del ciclo, ma non al suo interno, questo potrebbe offrire una maggiore chiarezza:

if(!something)
{
    for ( int i = 1; i < array.length; i++ )
    {...}
}

Ora immagina questo:

for ( int i = 1; i < array.length; i++ ) 
{
    // perform operations
    if(something) break;
    // perform more operations
}

Stiamo inviando un messaggio molto chiaro lì. Se something diventa true durante l'elaborazione dell'array, abbandonare l'intera operazione a questo punto. Per spostare il controllo nel campo delle condizioni devi fare:

for ( int i = 1; i < array.length && !something; i++ ) 
{
    // perform operations
    if(!something)
    {
        // perform more operations
    }
}

Probabilmente, il "messaggio" è diventato confuso e stiamo controllando la condizione di errore in due punti: cosa succede se la condizione di errore cambia e dimentichiamo uno di quei luoghi?

Ovviamente ci sono molte più forme diverse per loop e condizioni, tutte con le loro sfumature.

Scrivi il codice in modo che legga bene (questo è ovviamente in qualche modo soggettivo) e in modo conciso. Al di fuori di un draconiano insieme di linee guida per la codifica tutte "le regole" hanno delle eccezioni. Scrivi ciò che ritieni esprime la decisione di rendere al meglio e ridurre al minimo le possibilità di errori futuri.

    
risposta data 28.02.2018 - 15:21
fonte
2

Anche se può sembrare allettante perché vedi che tutte le condizioni nell'intestazione del ciclo e break possono confondere all'interno di grandi blocchi, un ciclo for è un modello in cui la maggior parte dei programmatori prevede il loop su un intervallo.

Specialmente da quando lo fai, aggiungere una condizione di rottura aggiuntiva può causare confusione ed è più difficile da vedere se è funzionalmente equivalente.

Spesso una buona alternativa è usare un ciclo while o do {} while che controlla entrambe le condizioni, assumendo che l'array abbia almeno un elemento.

int i=0
do {
    // some other code
    i++; 
} while(i < array.length && condition);

Usando un ciclo che controlla solo una condizione e non esegue il codice dall'intestazione del ciclo, si rende molto chiaro quando viene controllata la condizione e qual è la condizione effettiva e il codice con gli effetti collaterali.

    
risposta data 27.02.2018 - 14:59
fonte
1

Questo è male, poiché il codice è inteso per essere letto dagli umani - i cicli non-idiomatic for spesso sono confusi da leggere, il che significa che i bug sono più probabilmente nascosti qui, ma il codice originale potrebbe essere stato possibile per migliorare mentre è sempre breve e leggibile.

Se quello che vuoi è trovare un valore in un array (l'esempio di codice fornito è piuttosto generico, ma potrebbe anche essere questo schema), puoi semplicemente provare esplicitamente a utilizzare una funzione fornita da un linguaggio di programmazione specifico per quello scopo. Ad esempio (pseudo-codice, poiché non è stato specificato un linguaggio di programmazione, le soluzioni variano a seconda di una lingua specifica).

array.find(item -> item.valid)

Questo dovrebbe accorciare notevolmente la soluzione semplificandola mentre il tuo codice dice specificamente quello che ti serve.

    
risposta data 28.02.2018 - 10:45
fonte
1

Poche ragioni, a mio parere, dicono che non dovresti.

  1. Questo riduce la leggibilità.
  2. L'output potrebbe non essere lo stesso. Tuttavia, può essere eseguito con un ciclo do...while (non while ) poiché la condizione viene verificata dopo l'esecuzione di un codice.

Ma soprattutto, considera questo,

for(int i=0;i<array.length;i++){

    // Some other code
    if(condition1){
        break;
    }

    // Some more code
    if(condition1){
        break;
    }

    // Some even more code
}

Ora, puoi davvero ottenerlo aggiungendo queste condizioni in quella condizione for ?

    
risposta data 28.02.2018 - 05:53
fonte
0

Penso che rimuovere la break possa essere una buona idea, ma non salvare righe di codice.

Il vantaggio di scriverlo senza break è che la post-condizione del ciclo è ovvia ed esplicita. Al termine del ciclo ed eseguendo la riga successiva del programma, la condizione del ciclo i < array.length && !condition deve essere stata falsa. Pertanto, i era maggiore o uguale alla lunghezza dell'array, oppure condition è true.

Nella versione con break , c'è un trucco nascosto. La condizione del ciclo dice che il ciclo termina quando hai qualche altro codice per ogni indice di array valido, ma in effetti c'è un'istruzione break che potrebbe terminare il prima di ciò, e non lo vedrai a meno che tu non riveda il codice di loop molto attentamente. Inoltre, gli analizzatori automatici statici, tra cui l'ottimizzazione dei compilatori, potrebbero avere problemi a dedurre quali sono anche le post-condizioni del ciclo.

Questa era la ragione originale per cui Edgar Dijkstra ha scritto "Goto Considerated Harmful." Non era una questione di stile: uscire da un loop rende difficile ragionare formalmente sullo stato del programma. Ho scritto molte dichiarazioni di break; nella mia vita, e una volta da adulto, ho persino scritto un goto (per uscire da più livelli di un loop interno critico per le prestazioni e poi continuare con un altro ramo dell'albero di ricerca ). Tuttavia, sono molto più propenso a scrivere un'istruzione return condizionale da un ciclo (che non esegue mai il codice dopo il ciclo e quindi non è in grado di ridurre le sue condizioni) rispetto a break .

Postscript

In effetti, il refactoring del codice per dare lo stesso comportamento potrebbe effettivamente richiedere più righe di codice. Il tuo refactoring potrebbe essere valido per qualche altro codice o se possiamo dimostrare che condition inizierà false. Ma il tuo esempio è equivalente nel caso generale a:

if ( array.length > 0 ) {
  // Some other code.
}
for ( int i = 1; i < array.length && !condition; i++ ) {
  // Some other code.
}

Se un altro codice non è un one-liner, puoi quindi spostarlo su una funzione in modo da non ripetersi, e quindi hai quasi rifatto il tuo loop in una coda funzione -recursive.

Riconosco che questo non era il punto principale della tua domanda, però, e lo stavi mantenendo semplice.

    
risposta data 28.02.2018 - 01:21
fonte
0

Sì, ma la sintassi non è convenzionale e quindi è cattiva (tm)

Speriamo che la tua lingua supporti qualcosa come

while(condition) //condition being a condition which if true you want to continue looping
{
    do thing; //your conditionally executed code goes here
    i++; //you can use a counter if you want to reference array items in order of index
}
    
risposta data 27.02.2018 - 10:52
fonte
0

Se sei preoccupato che una dichiarazione di for eccessivamente complessa sia difficile da leggere, è sicuramente una preoccupazione valida. Anche lo spreco di spazio verticale è un problema (anche se meno critico).

(Personalmente non mi piace la regola secondo cui le dichiarazioni di if devono sempre avere parentesi, che è in gran parte la causa dello spazio verticale sprecato qui. Nota che il problema è sprecare spazio verticale Usare lo spazio verticale con linee significative non dovrebbe essere un problema.)

Un'opzione è dividerla su più linee. Questo è sicuramente quello che dovresti fare se stai usando istruzioni for davvero complesse, con più variabili e condizioni. (Questo è per me un uso migliore dello spazio verticale, dando a quante più linee possibili qualcosa di significativo.)

for (
   int i = 0, j = 0;
   i < array.length && !condition;
   i++, j++
) {
   // stuff
}

Un approccio migliore potrebbe essere quello di cercare di utilizzare una forma più dichiarativa e meno imperativa. Questo può variare a seconda della lingua, oppure potresti dover scrivere le tue funzioni per abilitare questo genere di cose. Dipende anche da cosa è effettivamente condition . Presumibilmente deve essere in grado di cambiare in qualche modo, a seconda di ciò che è nella matrice.

array
.takeWhile(condition)  // condition can be item => bool or (item, index) => bool
.forEach(doSomething); // doSomething can be item => void or (item, index) => void
    
risposta data 02.03.2018 - 18:54
fonte
0

Una cosa che è stata dimenticata: quando mi interrompo da un ciclo (non faccio tutte le possibili iterazioni, non importa quanto implementato), di solito è il caso che non solo voglio uscire dal ciclo, voglio anche sapere perché questo è successo. Ad esempio, se cerco un oggetto X, non solo interrompo il ciclo, ma voglio anche sapere che X è stato trovato e dove si trova.

In questa situazione, avere una condizione al di fuori del circuito è del tutto naturale. Quindi inizio con

bool found = false;
size_t foundIndex;

e nel ciclo scriverò

if (condition) {
    found = true;
    foundIndex = i;
    break;
}

con il ciclo for

for (i = 0; i < something && ! found; ++i)

Ora controllare la condizione nel ciclo è abbastanza naturale. Se esiste un'istruzione di "interruzione" dipende dal fatto che ci sia un'ulteriore elaborazione nel ciclo che si desidera evitare. Se è necessaria qualche elaborazione e altre elaborazioni no, potresti avere qualcosa di simile a

if (! found) { ... }

da qualche parte nel tuo ciclo.

    
risposta data 03.03.2018 - 15:42
fonte

Leggi altre domande sui tag