Ciclo Foreach con break / return vs. while-loop con invarianza e post-condizione esplicite

17

Questo è il modo più popolare (mi sembra) di verificare se un valore è in un array:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Tuttavia, in un libro che ho letto molti anni fa da, probabilmente, Wirth o Dijkstra, è stato detto che questo stile è migliore (se confrontato con un ciclo while con un'uscita all'interno ):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

In questo modo la condizione di uscita aggiuntiva diventa una parte esplicita del ciclo invariante, non ci sono condizioni nascoste e uscite all'interno del ciclo, tutto è più ovvio e più in un modo di programmazione strutturata. Generalmente preferivo quest'ultimo schema ogni volta che era possibile e usavo for -loop per iterare solo da a a b .

Eppure non posso dire che la prima versione sia meno chiara. Forse è ancora più chiaro e più facile da capire, almeno per i principianti. Quindi mi sto ancora ponendo la domanda su quale è migliore?

Forse qualcuno può dare una buona motivazione in favore di uno dei metodi?

Aggiornamento: Questa non è una questione di punti di ritorno di funzioni multiple, lambda o ricerca di un elemento in un array di per sé. Riguarda come scrivere loop con invarianti più complessi di una singola disuguaglianza.

Aggiornamento: OK, vedo il punto delle persone che rispondono e commentano: ho mixato nel ciclo foreach qui, che a sua volta è già molto più chiaro e leggibile di un ciclo while. Non avrei dovuto farlo. Ma questa è anche una domanda interessante, quindi lasciatela così com'è: foreach-loop e una condizione extra all'interno, o un ciclo while con un ciclo invariante esplicito e una post-condizione dopo. Sembra che il ciclo di foreach con una condizione e un exit / break sia vincente. Creerò una domanda aggiuntiva senza il ciclo foreach (per un elenco collegato).

    
posta Danila Piatov 10.08.2018 - 17:08
fonte

6 risposte

20

Penso che per cicli semplici, come questi, la prima sintassi standard sia molto più chiara. Alcune persone considerano che più ritorni confondano o un odore di codice, ma per un pezzo di codice così piccolo, non credo che questo sia un vero problema.

Diventa un po 'più discutibile per i loop più complessi. Se il contenuto del loop non può adattarsi allo schermo e presenta diversi ritorni nel ciclo, è necessario dimostrare che i punti di uscita multipli possono rendere il codice più difficile da mantenere. Ad esempio, se dovessi assicurarti che alcuni metodi di manutenzione dello stato funzionino prima di uscire dalla funzione, sarebbe facile perdere l'aggiunta ad una delle dichiarazioni di ritorno e potresti causare un bug. Se tutte le condizioni di fine possono essere controllate in un ciclo while, hai solo un punto di uscita e puoi aggiungere questo codice dopo di esso.

Detto questo, con i loop in particolare è bene provare a mettere più logica possibile in metodi separati. Questo evita molti casi in cui il secondo metodo avrebbe dei vantaggi. I cicli snelli con logica chiaramente separata avranno più importanza di quale di questi stili usi. Inoltre, se la maggior parte della base di codice della tua applicazione utilizza uno stile, dovresti mantenere quello stile.

    
risposta data 10.08.2018 - 18:37
fonte
56

Questo è facile.

Quasi nulla conta più della chiarezza per il lettore. La prima variante l'ho trovata incredibilmente semplice e chiara.

La seconda versione "migliorata", ho dovuto leggere più volte e assicurarmi che tutte le condizioni del bordo fossero corrette.

C'è ZERO DUBBIO che è uno stile di codifica migliore (il primo è molto meglio).

Ora - ciò che è CHIARO per le persone può variare da persona a persona. Non sono sicuro che ci siano degli standard oggettivi per questo (anche se postare su un forum come questo e ottenere input da una varietà di persone può aiutare).

In questo caso particolare, tuttavia, posso dirti perché il primo algoritmo è più chiaro: so come si presenta e come funziona l'iterazione del C ++ su una sintassi del contenitore. L'ho interiorizzato. Qualcuno UNFAMILIAR (la sua nuova sintassi) con quella sintassi potrebbe preferire la seconda variante.

Ma una volta che conosci e capisci la nuova sintassi, è un concetto di base che puoi semplicemente usare. Con l'approccio di iterazione del ciclo (secondo), è necessario controllare attentamente che l'utente stia verificando CORRETTAMENTE tutte le condizioni del bordo per eseguire il loop sull'intero array (ad esempio, meno di un uguale o meno uguale, lo stesso indice utilizzato per il test e per l'indicizzazione ecc.)

    
risposta data 10.08.2018 - 17:31
fonte
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[…] everything is more obvious and more in a structured-programming way.

Non proprio. La variabile i esiste al di fuori del ciclo while qui ed è quindi parte dell'ambito esterno, mentre (il linguaggio desiderato) x di for -loop esiste solo nell'ambito del ciclo. L'ambito è un modo molto importante per introdurre la struttura alla programmazione.

    
risposta data 10.08.2018 - 19:16
fonte
2

I due cicli hanno una semantica diversa:

  • Il primo ciclo risponde semplicemente a una semplice domanda si / no: "La matrice contiene l'oggetto che sto cercando?" Lo fa nel modo più breve possibile.

  • Il secondo ciclo risponde alla domanda: "Se la matrice contiene l'oggetto che sto cercando, qual è l'indice della prima partita? " Di nuovo, lo fa nel modo più breve possibile.

Dato che la risposta alla seconda domanda fornisce strettamente più informazioni rispetto alla risposta al primo, è possibile scegliere di rispondere alla seconda domanda e quindi derivare la risposta della prima domanda. Questo è ciò che la linea return i < array.length; fa, comunque.

Credo che di solito sia preferibile utilizzare solo lo strumento adatto allo scopo , a meno che non sia possibile riutilizzare uno strumento già esistente e più flessibile. Cioè:.

  • Utilizzare la prima variante del ciclo va bene.
  • La modifica della prima variante per impostare solo una variabile bool e l'interruzione è anch'essa valida. (Evita la seconda istruzione return , la risposta è disponibile in una variabile invece di una funzione restituita.)
  • L'utilizzo di std::find va bene (riutilizzo del codice!).
  • Tuttavia, codificare esplicitamente un risultato e quindi ridurre la risposta a bool non lo è.
risposta data 11.08.2018 - 10:17
fonte
2

Suggerirò una terza opzione del tutto:

return array.find(value);

Ci sono molti diversi motivi per iterare su un array: controlla se esiste un valore specifico, trasforma l'array in un altro array, calcola un valore aggregato, filtra alcuni valori fuori dall'array ... Se usi un ciclo for for plain , non è chiaro a colpo d'occhio in che modo viene utilizzato il ciclo for. Tuttavia, la maggior parte dei linguaggi moderni dispone di API ricche sulle strutture dei dati dell'array che rendono molto espliciti questi diversi intenti.

Confronta la trasformazione di un array in un altro con un ciclo for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

e utilizzo di una funzione map di stile JavaScript:

array.map((value) => value * 2);

O sommando un array:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

vs

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Quanto tempo ti ci vuole per capire cosa fa?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

vs

array.filter((value) => (value >= 0));

In tutti e tre i casi, mentre il ciclo for è certamente leggibile, è necessario dedicare alcuni minuti a capire come viene utilizzato il ciclo for e controllare che tutti i contatori e le condizioni di uscita siano corretti. Le moderne funzioni in stile lambda rendono estremamente espliciti gli scopi dei loop e sai per certo che le funzioni API chiamate sono implementate correttamente.

La maggior parte delle lingue moderne, tra cui JavaScript , Ruby , C # e Java , usa questo stile di interazione funzionale con array e collezioni simili.

In generale, anche se non penso che l'uso di loop per i loop sia necessariamente sbagliato, ed è una questione di gusto personale, mi sono trovato a preferire strongmente l'utilizzo di questo stile di lavoro con gli array. Questo è specificamente dovuto alla maggiore chiarezza nel determinare cosa sta facendo ogni ciclo. Se la tua lingua ha caratteristiche o strumenti simili nelle sue librerie standard, ti suggerisco di prendere in considerazione anche l'adozione di questo stile!

    
risposta data 11.08.2018 - 00:11
fonte
-2

Tutto si riduce esattamente a ciò che si intende per "migliore". Per i programmatori pratici, in genere significa efficiente: in questo caso, l'uscita direttamente dal loop evita un confronto extra e il ritorno di una costante booleana evita un confronto duplicato; questo risparmia cicli. Dijkstra è più interessato a creare codice che più facile da dimostrare . [mi è sembrato che l'educazione al CS in Europa tenga molto più seriamente la "dimostrazione della correttezza del codice" rispetto all'istruzione CS negli Stati Uniti, dove le forze economiche tendono a dominare la pratica della codifica]

    
risposta data 10.08.2018 - 22:43
fonte