Foreach-Schleife mit break / return vs. while-Schleife mit expliziter Invariante und Nachbedingung

17

Dies ist die (meiner Meinung nach) beliebteste Methode , um zu überprüfen, ob ein Wert in einem Array enthalten ist:

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

In einem Buch, das ich vor vielen Jahren wahrscheinlich von Wirth oder Dijkstra gelesen habe, hieß es jedoch, dass dieser Stil besser ist (im Vergleich zu einer while-Schleife mit einem Ausgang im Inneren):

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

Auf diese Weise wird die zusätzliche Exit-Bedingung ein expliziter Bestandteil der Schleifeninvariante, es gibt keine versteckten Bedingungen und Exits innerhalb der Schleife, alles ist offensichtlicher und strukturierter. Ich bevorzuge im Allgemeinen das letztere Muster, wann immer es möglich ist, und forbenutze die -Schleife, um nur von abis zu iterieren b.

Und doch kann ich nicht sagen, dass die erste Version weniger klar ist. Vielleicht ist es sogar noch klarer und verständlicher, zumindest für Anfänger. Also frage ich mich immer noch, welches besser ist?

Vielleicht kann jemand eine gute Begründung für eine der Methoden geben?

Update: Hierbei handelt es sich nicht um mehrere Funktionsrückgabepunkte, Lambdas oder das Finden eines Elements in einem Array per se. Es geht darum, Schleifen mit komplexeren Invarianten als einer einzelnen Ungleichung zu schreiben.

Update: OK, ich sehe den Punkt von Leuten, die antworten und kommentieren: Ich habe hier die foreach-Schleife eingemischt, die selbst schon viel klarer und lesbarer ist als eine while-Schleife. Ich hätte das nicht tun sollen. Aber dies ist auch eine interessante Frage, also lassen wir es wie es ist: foreach-loop und eine zusätzliche Bedingung im Inneren oder eine while-Schleife mit einer expliziten Schleifeninvariante und einer Nachbedingung danach. Es scheint, dass die foreach-Schleife mit einer Bedingung und einem Exit / Break gewinnt. Ich werde eine zusätzliche Frage ohne die foreach-Schleife erstellen (für eine verknüpfte Liste).

Danila Piatov
quelle
2
Die hier zitierten Codebeispiele mischen verschiedene Probleme. Frühe und mehrfache Rückgaben (was für mich auf die Größe der Methode (nicht gezeigt) zutrifft), Array-Suche (was eine Diskussion mit Lambdas erfordert), foreach vs. direkte Indizierung ... Diese Frage wäre klarer und einfacher zu beantworten Beantworten Sie diese Frage, wenn Sie sich jeweils nur auf eines dieser Probleme konzentriert haben.
Erik Eidt
1
Ich weiß, das sind Beispiele, aber es gibt Sprachen, die APIs haben, um genau diesen Anwendungsfall zu behandeln. Iecollection.contains(foo)
Berin Loritsch
2
Vielleicht möchten Sie das Buch suchen und es jetzt erneut lesen, um zu sehen, was es tatsächlich gesagt hat.
Thorbjørn Ravn Andersen
1
"Besser" ist ein sehr subjektives Wort. Dennoch kann man auf einen Blick erkennen, was die erste Version tut. Dass die zweite Version genau dasselbe tut, bedarf einiger Prüfung.
David Hammen

Antworten:

19

Ich denke, für einfache Schleifen wie diese ist die Standard-First-Syntax viel klarer. Einige Leute halten Mehrfachrückgaben für verwirrend oder einen Code-Geruch, aber für ein so kleines Stück Code glaube ich nicht, dass dies ein echtes Problem ist.

Bei komplexeren Loops wird es ein bisschen fragwürdiger. Wenn der Inhalt der Schleife nicht auf Ihren Bildschirm passt und mehrere Rückgaben in der Schleife enthalten sind, muss argumentiert werden, dass die mehreren Austrittspunkte die Pflege des Codes erschweren können. Wenn Sie zum Beispiel sicherstellen müssten, dass vor dem Beenden der Funktion eine Statusverwaltungsmethode ausgeführt wurde, würde es leicht fehlen, sie einer der return-Anweisungen hinzuzufügen, und Sie würden einen Fehler verursachen. Wenn alle Endebedingungen in einer while-Schleife überprüft werden können, haben Sie nur einen Exit-Punkt und können diesen Code danach hinzufügen.

Vor allem bei Schleifen ist es jedoch gut zu versuchen, so viel Logik wie möglich in separate Methoden zu stecken. Dies vermeidet viele Fälle, in denen die zweite Methode Vorteile hätte. Lean-Loops mit klar getrennter Logik sind wichtiger als der von Ihnen verwendete Stil. Wenn der größte Teil der Codebasis Ihrer Anwendung einen Stil verwendet, sollten Sie diesen Stil beibehalten.

Nathanael
quelle
56

Das ist einfach.

Für den Leser ist fast nichts wichtiger als Klarheit. Die erste Variante fand ich unglaublich einfach und übersichtlich.

Die zweite 'verbesserte' Version musste ich mehrmals lesen und sicherstellen, dass alle Randbedingungen stimmen.

Es gibt ZERO DOUBT mit einem besseren Codierungsstil (der erste ist viel besser).

Nun - was für Menschen KLAR ist, kann von Person zu Person unterschiedlich sein. Ich bin mir nicht sicher, ob es dafür objektive Standards gibt (obwohl das Posten in einem Forum wie diesem und das Erhalten einer Vielzahl von Beiträgen von Leuten helfen kann).

In diesem speziellen Fall kann ich Ihnen jedoch sagen, warum der erste Algorithmus klarer ist: Ich weiß, wie die C ++ - Iteration über eine Containersyntax aussieht und funktioniert. Ich habe es verinnerlicht. Jemand UNFAMILIAR (seine neue Syntax) mit dieser Syntax bevorzugt möglicherweise die zweite Variante.

Aber sobald Sie diese neue Syntax kennen und verstehen, können Sie sie einfach anwenden. Bei der (zweiten) Methode der Schleifeniteration müssen Sie sorgfältig prüfen, ob der Benutzer RICHTIG nach allen Kantenbedingungen sucht, um das gesamte Array zu durchlaufen (z. B. weniger als anstelle von weniger oder gleichem Index, der für test und verwendet wird) zur Indizierung etc).

Lewis Pringle
quelle
4
Neu ist relativ, wie es schon im 2011er Standard war. Auch die zweite Demo ist offensichtlich nicht C ++.
Deduplizierer
Eine alternative Lösung , wenn Sie einen einzigen Austrittspunkt verwenden , wäre ein Flag zu setzen longerLength = true, dann return longerLength.
Cullub
@ Deduplicator Warum ist die zweite Demo nicht C ++? Ich verstehe nicht warum nicht oder vermisse ich etwas Offensichtliches?
Rakete1111
2
@ Rakete1111 Raw-Arrays haben keine Eigenschaften wie length. Wenn es tatsächlich als Array und nicht als Zeiger deklariert wurde, könnte es verwendet werden sizeof, oder wenn es eine std::arraykorrekte Member-Funktion ist size(), gibt es keine lengthEigenschaft.
IllusiveBrian
@IllusiveBrian: sizeofwäre in Bytes ... Die allgemeinste seit C ++ 17 ist std::size().
Deduplicator
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[...] alles ist offensichtlich und mehr in einer strukturierten Programmierung Art und Weise.

Nicht ganz. Die Variable iexistiert hier außerhalb der while-Schleife und ist somit Teil des äußeren Gültigkeitsbereichs, während (beabsichtigtes Wortspiel) xder for-Loop nur innerhalb des Gültigkeitsbereichs der Schleife existiert. Umfang ist ein sehr wichtiger Weg, um Struktur in die Programmierung einzuführen.

Null
quelle
1
@ruakh Ich bin mir nicht sicher, was ich aus deinem Kommentar entfernen soll. Es wirkt etwas passiv-aggressiv, als würde meine Antwort dem widersprechen, was auf der Wiki-Seite steht. Bitte erläutern Sie.
null
"Strukturierte Programmierung" ist ein Fachbegriff mit einer bestimmten Bedeutung, und das OP ist objektiv korrekt, dass Version 2 den Regeln der strukturierten Programmierung entspricht, während Version 1 dies nicht tut. Aus Ihrer Antwort ging hervor, dass Sie mit dem Begriff Kunst nicht vertraut waren und den Begriff wörtlich auslegten. Ich bin mir nicht sicher, warum mein Kommentar passiv-aggressiv wirkt. Ich meinte es einfach als informativ.
Ruakh
@ruakh Ich stimme nicht zu, dass Version 2 den Regeln in jeder Hinsicht besser entspricht, und erklärte dies in meiner Antwort.
null
Sie sagen "Ich stimme nicht zu", als wäre es eine subjektive Sache, aber es ist nicht so. Die Rückkehr aus einer Schleife heraus ist ein kategorischer Verstoß gegen die Regeln der strukturierten Programmierung. Ich bin sicher, dass viele Liebhaber der strukturierten Programmierung Fans von Variablen mit minimalem Gültigkeitsbereich sind. Wenn Sie jedoch den Gültigkeitsbereich einer Variablen verringern, indem Sie von der strukturierten Programmierung abweichen, haben Sie sich von der strukturierten Programmierung, Punkt und Reduzierung des Gültigkeitsbereichs der Variablen verabschiedet Das.
Ruakh
2

Die beiden Schleifen haben unterschiedliche Semantik:

  • Die erste Schleife beantwortet einfach eine einfache Ja / Nein-Frage: "Enthält das Array das gesuchte Objekt?" Dies geschieht so kurz wie möglich.

  • Die zweite Schleife beantwortet die Frage: "Wenn das Array das gesuchte Objekt enthält, wie lautet der Index der ersten Übereinstimmung?" Auch dies geschieht so kurz wie möglich.

Da die Antwort auf die zweite Frage streng mehr Informationen enthält als die Antwort auf die erste, können Sie die zweite Frage beantworten und dann die Antwort auf die erste Frage ableiten. Das ist was die Liniereturn i < array.length; jedenfalls.

Ich glaube, dass es normalerweise am besten ist, nur das Werkzeug zu verwenden, das zum Zweck passt sei denn, Sie können ein bereits vorhandenes, flexibleres Werkzeug wiederverwenden. Dh:

  • Die Verwendung der ersten Variante der Schleife ist in Ordnung.
  • Es boolist auch in Ordnung, die erste Variante so zu ändern, dass nur eine Variable und eine Unterbrechung festgelegt werden. (Vermeidet Sekundereturn Anweisung, die Antwort ist in einer Variablen anstelle einer Funktionsrückgabe verfügbar.)
  • Die Verwendung std::findist in Ordnung (Wiederverwendung von Code!).
  • Das explizite Codieren eines Funds und das Reduzieren der Antwort auf a boolist jedoch nicht der Fall .
In cmaster stellst du Monica wieder her
quelle
Wäre nett, wenn die Downvoter einen Kommentar hinterlassen würden ...
cmaster - monica wieder einsetzen
2

Ich werde insgesamt eine dritte Option vorschlagen:

return array.find(value);

Es gibt viele verschiedene Gründe, ein Array zu durchlaufen: Überprüfen Sie, ob ein bestimmter Wert vorhanden ist, transformieren Sie das Array in ein anderes Array, berechnen Sie einen Aggregatwert, filtern Sie einige Werte aus dem Array heraus ... Wenn Sie eine einfache for-Schleife verwenden, ist dies unklar auf einen Blick speziell, wie die for-Schleife verwendet wird. Die meisten modernen Sprachen haben jedoch umfangreiche APIs in ihren Array-Datenstrukturen, die diese unterschiedlichen Absichten sehr deutlich machen.

Vergleichen Sie die Umwandlung eines Arrays in ein anderes mit einer for-Schleife:

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

und Verwendung einer JavaScript-ähnlichen mapFunktion:

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

Oder summieren Sie ein Array:

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

gegen:

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

Wie lange brauchen Sie, um zu verstehen, was dies bewirkt?

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

gegen

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

In allen drei Fällen, während die for-Schleife sicher lesbar ist, müssen Sie einige Momente verbringen, um herauszufinden, wie die for-Schleife verwendet wird, und um zu überprüfen, ob alle Zähler und Ausgangsbedingungen korrekt sind. Die modernen Funktionen im Lambda-Stil machen die Zwecke der Schleifen sehr deutlich, und Sie wissen mit Sicherheit, dass die aufgerufenen API-Funktionen korrekt implementiert sind.

Die meisten modernen Sprachen, einschließlich JavaScript , Ruby , C # und Java , verwenden diesen Stil der funktionalen Interaktion mit Arrays und ähnlichen Sammlungen.

Im Allgemeinen halte ich die Verwendung von for-Schleifen zwar nicht unbedingt für falsch, und es ist eine Frage des persönlichen Geschmacks. Ich habe mich jedoch entschieden für die Verwendung dieses Arbeitsstils mit Arrays entschieden. Dies ist insbesondere auf die größere Klarheit bei der Bestimmung der einzelnen Schleifentätigkeiten zurückzuführen. Wenn Ihre Sprache ähnliche Funktionen oder Tools in ihren Standardbibliotheken hat, empfehlen wir Ihnen, diesen Stil ebenfalls zu übernehmen!

Kevin - Setzen Sie Monica wieder ein
quelle
2
Das Empfehlen array.findwirft die Frage auf, da wir dann die beste Art der Implementierung besprechen müssen array.find. Sofern Sie keine Hardware mit integriertem findBetrieb verwenden, müssen wir dort eine Schleife schreiben.
Barmar
2
@Barmar Ich bin anderer Meinung. Wie ich in meiner Antwort angedeutet habe, bieten viele häufig verwendete Sprachen Funktionen wie findin ihren Standardbibliotheken. Zweifellos implementieren diese Bibliotheken findund ihre Verwandten for-Schleifen, aber das ist es, was eine gute Funktion tut: Sie abstrahiert die technischen Details vom Verbraucher der Funktion, sodass der Programmierer nicht über diese Details nachdenken muss. Obwohl findes wahrscheinlich mit einer for-Schleife implementiert ist, hilft es dennoch, den Code besser lesbar zu machen, und da es häufig in der Standardbibliothek enthalten ist, erhöht die Verwendung des Codes weder den Aufwand noch das Risiko.
Kevin - Reinstate Monica
4
Ein Softwareentwickler muss diese Bibliotheken jedoch implementieren. Gilt nicht dasselbe Prinzip der Softwareentwicklung für Bibliotheksautoren wie für Anwendungsprogrammierer? Die Frage ist über das Schreiben von Schleifen im Allgemeinen, nicht der beste Weg, um nach einem Array-Element in einer bestimmten Sprache zu suchen
Barmar
4
Anders ausgedrückt, die Suche nach einem Array-Element ist nur ein einfaches Beispiel, anhand dessen er die verschiedenen Schleifentechniken demonstrierte.
Barmar
-2

Es läuft alles auf genau das hinaus, was mit "besser" gemeint ist. Für praktische Programmierer bedeutet dies im Allgemeinen effizient, dh in diesem Fall wird durch das Verlassen der Schleife ein zusätzlicher Vergleich vermieden, und durch das Zurückgeben einer Booleschen Konstante wird ein doppelter Vergleich vermieden. das spart zyklen. Dijkstra ist mehr darum bemüht, Code zu erstellen, der sich leichter als richtig erweisen lässt . [Ich habe den Eindruck, dass die CS-Ausbildung in Europa den Nachweis der Code-Korrektheit weitaus ernster nimmt als die CS-Ausbildung in den USA, wo die wirtschaftlichen Kräfte die Codierungspraxis dominieren.]

PMar
quelle
3
In Bezug auf die Leistung sind beide Schleifen ziemlich gleichwertig - beide haben zwei Vergleiche.
Danila Piatov
1
Wenn man sich wirklich für die Leistung interessiert, würde man einen schnelleren Algorithmus verwenden. zB sortiere das Array und führe eine binäre Suche durch oder verwende eine Hashtabelle.
user949300
Danila - Sie wissen nicht, welche Datenstruktur dahinter steckt. Ein Iterator ist immer schnell. Der indizierte Zugriff kann eine lineare Zeit sein und die Länge muss nicht einmal vorhanden sein.
gnasher729