Foreach-loop con break / return vs. while-loop con invariante esplicito e post-condition


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 di, probabilmente, Wirth o Dijkstra, è stato detto che questo stile è migliore (rispetto a un ciclo di loop 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 dell'invariante del ciclo, non ci sono condizioni nascoste ed esce all'interno del ciclo, tutto è più ovvio e più in un modo di programmazione strutturata. In genere ho preferito questo secondo schema ogni volta che era possibile e forho usato il -loop per iterare solo da aa 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 sia la migliore?

Forse qualcuno può dare una buona logica a favore di uno dei metodi?

Aggiornamento: questa non è una questione di punti di ritorno a funzioni multiple, lambda o trovare 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 inserito qui il ciclo foreach, che di per sé è già molto più chiaro e leggibile di un ciclo while. Non avrei dovuto farlo. Ma questa è anche una domanda interessante, quindi lasciamola così com'è: foreach-loop e una condizione extra all'interno, o un ciclo while con un invariante loop esplicito e una post-condizione dopo. Sembra che il ciclo foreach con una condizione e un'uscita / interruzione stia vincendo. Creerò una domanda aggiuntiva senza il ciclo foreach (per un elenco collegato).


2
Gli esempi di codice citati qui mescolano diversi problemi. Restituzioni anticipate e multiple (che per me vanno alle dimensioni del metodo (non mostrato)), ricerca di array (che richiede una discussione che coinvolge lambda), foreach vs. indicizzazione diretta ... Questa domanda sarebbe più chiara e più facile da rispondere se si concentrava solo su uno di questi problemi alla volta.
Erik Eidt,


1
So che sono esempi, ma ci sono lingue che hanno API per gestire esattamente quel caso d'uso. collection.contains(foo)
Vale a

2
Potresti voler trovare il libro e rileggerlo ora per vedere cosa ha effettivamente detto.
Thorbjørn Ravn Andersen,

1
"Meglio" è una parola altamente soggettiva. Detto questo, si può dire a colpo d'occhio cosa sta facendo la prima versione. Che la seconda versione faccia esattamente la stessa cosa richiede un po 'di controllo.
David Hammen,

Risposte:


19

Penso che per semplici loop, come questi, la prima sintassi standard sia molto più chiara. Alcune persone considerano i rendimenti multipli confusi 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 loop più complessi. Se il contenuto del loop non può adattarsi allo schermo e ha diversi ritorni nel loop, c'è un argomento da sostenere che i punti di uscita multipli possono rendere il codice più difficile da mantenere. Ad esempio, se si dovesse garantire l'esecuzione di un metodo di manutenzione dello stato prima di uscire dalla funzione, sarebbe facile perdere l'aggiunta a una delle dichiarazioni di ritorno e si causerebbe un errore. Se tutte le condizioni finali possono essere verificate in un ciclo while, hai solo un punto di uscita e puoi aggiungere questo codice dopo di esso.

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


56

Questo è facile.

Quasi nulla conta più della chiarezza per il lettore. La prima variante che ho trovato incredibilmente semplice e chiara.

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

C'è ZERO DOUBT che è lo 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 standard oggettivi per questo (anche se pubblicare su un forum come questo e ottenere una varietà di input delle persone può aiutare).

In questo caso particolare, tuttavia, posso dirti perché il primo algoritmo è più chiaro: so come appare e fa l'iterazione 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 comprendi quella nuova sintassi, è un concetto di base che puoi semplicemente usare. Con l'approccio di iterazione in loop (secondo), è necessario verificare attentamente che l'utente stia verificando CORRETTAMENTE tutte le condizioni dei bordi per eseguire il loop sull'intero array (ad es. Meno che al posto di meno o uguale, stesso indice usato per test e per indicizzazione ecc.).


4
Il nuovo è relativo, come era già nello standard 2011. Inoltre, la seconda demo non è ovviamente C ++.
Deduplicatore

Una soluzione alternativa se si desidera utilizzare un singolo punto di uscita sarebbe quella di impostare un flag longerLength = true, quindi return longerLength.
Cullub,

@Deduplicator Perché la seconda demo C ++ non è? Non vedo perché no o mi sto perdendo qualcosa di ovvio?
Rakete1111,

2
@ Rakete1111 Gli array grezzi non hanno proprietà simili length. Se fosse effettivamente dichiarato come un array e non come un puntatore, potrebbero usare sizeof, o se fosse una std::arrayfunzione membro corretta size(), non c'è una lengthproprietà.
IllusiveBrian,

@IllusiveBrian: sizeofsarebbe in byte ... Il più generico dal C ++ 17 è std::size().
Deduplicatore

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

[…] Tutto è più ovvio e più in un modo di programmazione strutturata.

Non proprio. La variabile iesiste al di fuori del ciclo while qui e fa quindi parte dell'ambito esterno, mentre (il gioco xdi forparole previsto) del -loop esiste solo nell'ambito del ciclo. L'ambito è un modo molto importante per introdurre la struttura nella programmazione.



1
@ruakh Non sono sicuro di cosa togliere dal tuo commento. Si presenta in qualche modo passivo-aggressivo, come se la mia risposta si opponesse a ciò che è scritto sulla pagina wiki. Per favore, elabora.
null

"Programmazione strutturata" è un termine d'arte con un significato specifico e l'OP è oggettivamente corretto che la versione 2 sia conforme alle regole della programmazione strutturata mentre la versione 1 non lo fa. Dalla tua risposta, sembrava che tu non avessi familiarità con il termine dell'arte e che interpretassi letteralmente il termine. Non sono sicuro del perché il mio commento sia passivo-aggressivo; Lo intendevo semplicemente come informativo.
Ruakh,

@ruakh Non sono d'accordo sul fatto che la versione 2 sia conforme alle regole più sotto ogni aspetto e lo abbia spiegato nella mia risposta.
null

Dici "Non sono d'accordo" come se fosse una cosa soggettiva, ma non lo è. Il ritorno dall'interno di un ciclo è una violazione categorica delle regole della programmazione strutturata. Sono sicuro che molti appassionati di programmazione strutturata sono fan di variabili con ambito minimo, ma se riduci l'ambito di una variabile allontanandoti dalla programmazione strutturata, allora sei partito dalla programmazione strutturata, dal punto e riducendo l'ambito della variabile non si annulla quello.
Ruakh,

2

I due loop hanno una semantica diversa:

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

  • Il secondo ciclo risponde alla domanda: "Se l'array contiene l'oggetto che sto cercando, qual è l'indice della prima corrispondenza?" Ancora una volta, lo fa nel modo più breve possibile.

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

Ritengo che di solito sia meglio utilizzare lo strumento adatto allo scopo, a meno che non sia possibile riutilizzare uno strumento già esistente e più flessibile. Vale a dire:

  • L'uso della prima variante del loop va bene.
  • Anche la modifica della prima variante per impostare semplicemente una boolvariabile e interruzione va bene. (Evita la seconda returnistruzione, la risposta è disponibile in una variabile anziché in un ritorno di funzione.)
  • L'uso std::findva bene (riutilizzo del codice!).
  • Tuttavia, codificare esplicitamente una ricerca e quindi ridurre la risposta a una boolnon lo è.

Sarebbe bello se i downvoter lasciassero un commento ...
cmaster - ripristina monica

2

Suggerirò una terza opzione del tutto:

return array.find(value);

Esistono molte ragioni diverse 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 dall'array ... Se usi un plain per loop, non è chiaro a colpo d'occhio in particolare come viene utilizzato il ciclo for. Tuttavia, i linguaggi più moderni hanno API ricche sulle loro strutture di dati di 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 usando una mapfunzione in 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];
}

contro:

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

Quanto tempo impieghi a capire cosa fa questo?

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

contro

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

In tutti e tre i casi, mentre il ciclo for è certamente leggibile, devi dedicare qualche istante per capire come viene usato il ciclo for e verificare 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 si sa con certezza che le funzioni API chiamate vengono implementate correttamente.

La maggior parte dei linguaggi moderni, inclusi JavaScript , Ruby , C # e Java , usano questo stile di interazione funzionale con array e raccolte simili.

In generale, anche se non penso che usare per i loop sia necessariamente sbagliato, ed è una questione di gusti personali, mi sono trovato fortemente a preferire questo stile di lavoro con le matrici. Ciò è dovuto in particolare 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 di adottare anche questo stile!


2
Raccomandare array.findfa sorgere la domanda, poiché dobbiamo quindi discutere il modo migliore per implementare array.find. A meno che tu non stia utilizzando l'hardware con un'operazione integrata find, dobbiamo scrivere un loop lì.
Barmar,

2
@Barmar Non sono d'accordo. Come ho indicato nella mia risposta, molte lingue molto utilizzate forniscono funzioni come findnelle loro librerie standard. Indubbiamente, queste librerie implementano finde il suo parente utilizza per i loop, ma è ciò che fa una buona funzione: estrae i dettagli tecnici dal consumatore della funzione, permettendo a quel programmatore di non dover pensare a quei dettagli. Quindi, sebbene findsia probabilmente implementato con un ciclo for, aiuta comunque a rendere il codice più leggibile e, poiché è spesso nella libreria standard, il suo utilizzo non comporta alcun sovraccarico o rischio significativo.
Kevin,

4
Ma un ingegnere del software deve implementare queste librerie. Gli stessi principi di ingegneria del software non si applicano agli autori delle biblioteche dei programmatori di applicazioni? La domanda riguarda la scrittura di loop in generale, non il modo migliore per cercare un elemento array in una lingua particolare
Barmar

4
Per dirla in altro modo, la ricerca di un elemento array è solo un semplice esempio che ha usato per dimostrare le diverse tecniche di loop.
Barmar,

-2

Tutto si riduce esattamente a cosa si intende per "migliore". Per i programmatori pratici, in genere significa efficiente - vale a dire, in questo caso, uscire direttamente dal loop evita un ulteriore confronto e restituire una costante booleana evita un confronto duplicato; questo salva cicli. Dijkstra è più interessato a rendere il codice più facile da dimostrare corretto . [mi è sembrato che l'educazione alla CS in Europa prenda la "dimostrazione della correttezza del codice" molto più seriamente dell'educazione alla CS negli Stati Uniti, dove le forze economiche tendono a dominare la pratica del codice]


3
PMar, per quanto riguarda le prestazioni, entrambi i loop sono praticamente equivalenti: entrambi hanno due confronti.
Danila Piatov,

1
Se uno si preoccupasse davvero delle prestazioni, userebbe un algoritmo più veloce. ad es. ordinare l'array ed eseguire una ricerca binaria o utilizzare un Hashtable.
user949300

Danila - non sai quale struttura di dati ci sia dietro. Un iteratore è sempre veloce. L'accesso indicizzato può essere tempo lineare e la lunghezza non deve nemmeno esistere.
gnasher729,
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.