So strukturieren Sie eine Schleife, die sich bis zum Erfolg wiederholt und Fehler behandelt

8

Ich bin ein Autodidakt. Ich habe vor ungefähr 1,5 Jahren angefangen zu programmieren. Jetzt habe ich angefangen, Programmierunterricht in der Schule zu haben. Wir haben Programmierkurse für 1/2 Jahr und werden jetzt noch 1/2 haben.

In den Klassen lernen wir, in C ++ zu programmieren (eine Sprache, die ich bereits gut kannte, bevor wir anfingen).

Ich hatte während dieses Kurses keine Schwierigkeiten, aber es gibt ein wiederkehrendes Problem, für das ich keine klare Lösung finden konnte.

Das Problem ist wie folgt (im Pseudocode):

 do something
 if something failed:
     handle the error
     try something (line 1) again
 else:
     we are done!

Hier ist ein Beispiel in C ++

Der Code fordert den Benutzer auf, eine Nummer einzugeben, bis die Eingabe gültig ist. Hiermit wird cin.fail()überprüft, ob die Eingabe ungültig ist. Wann cin.fail()ist truemuss ich anrufen cin.clear()und der cin.ignore()Lage sein, weiterhin eine Eingabe von dem Strom zu bekommen.

Mir ist bekannt, dass dieser Code nicht überprüft EOF. Von den Programmen, die wir geschrieben haben, wird nicht erwartet, dass sie dies tun.

So habe ich den Code in einer meiner Aufgaben in der Schule geschrieben:

for (;;) {
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        continue;
    }
    break;
}

Mein Lehrer sagte, dass ich das nicht benutzen sollte breakund continuemag. Er schlug vor, stattdessen eine reguläre Schleife whileoder eine do ... whileSchleife zu verwenden.

Es scheint mir, dass die Verwendung von breakund continueder einfachste Weg ist, diese Art von Schleife darzustellen. Ich habe tatsächlich eine ganze Weile darüber nachgedacht, aber nicht wirklich eine klarere Lösung gefunden.

Ich denke, er wollte, dass ich so etwas mache:

do {
    cout << ": ";
    cin >> input;
    bool fail = cin.fail();
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (fail);

Für mich scheint diese Version viel komplexer zu sein, da wir jetzt auch eine Variable haben, die aufgerufen wird fail, um den Überblick zu behalten, und die Überprüfung auf Eingabefehler zweimal statt nur einmal durchgeführt wird.

Ich dachte mir auch, dass ich den Code so schreiben kann (Missbrauch der Kurzschlussauswertung):

do {
    cout << ": ";
    cin >> input;
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (cin.fail() && (cin.clear(), cin.ignore(512, '\n', true);

Diese Version funktioniert genau wie die anderen. Es wird nicht verwendet breakoder continueund der cin.fail()Test wird nur einmal durchgeführt. Es erscheint mir jedoch nicht richtig, die "Kurzschlussbewertungsregel" so zu missbrauchen. Ich glaube auch nicht, dass es meinem Lehrer gefallen würde.

Dieses Problem betrifft nicht nur die cin.fail()Überprüfung. Ich habe breakund continuemag dies für viele andere Fälle, in denen ein Satz von Codes wiederholt wird, bis eine Bedingung erfüllt ist, bei der auch etwas getan werden muss, wenn die Bedingung nicht erfüllt ist (wie das Aufrufen cin.clear()und cin.ignore(...)aus dem cin.fail()Beispiel).

Ich habe breakund continuewährend des gesamten Kurses weiter verwendet und jetzt hat mein Lehrer aufgehört, sich darüber zu beschweren.

Was ist deine Meinung dazu?

Glaubst du, mein Lehrer hat recht?

Kennen Sie einen besseren Weg, um diese Art von Problem darzustellen?

wefwefa3
quelle
6
Sie schleifen nicht für immer. Warum verwenden Sie also etwas, das impliziert, dass Sie für immer schleifen? Eine Schleife für immer ist nicht immer eine schlechte Wahl (obwohl dies normalerweise der Fall ist). In diesem Fall ist sie offensichtlich eine schlechte Wahl, da sie die falsche Absicht des Codes kommuniziert. Die zweite Methode ist näher an der Absicht des Codes, daher ist es besser. Der Code im zweiten Snippet könnte mit minimaler Reorganisation etwas aufgeräumter und leichter lesbar gemacht werden.
Dunk
3
@Dunk Das scheint ein bisschen dogmatisch. Es ist tatsächlich sehr selten, dass eine Schleife für immer benötigt wird, außer auf der obersten Ebene interaktiver Programme. Ich sehe while (true)und sofort annehmen , dass es ein break, returnoder throwirgendwo da drin. Die Einführung einer zusätzlichen Variablen, die im Auge behalten werden muss, um einer breakoder auszuweichen, continueist kontraproduktiv. Ich würde argumentieren, dass das Problem mit dem Anfangscode darin besteht, dass die Schleife eine Iteration normalerweise nie beendet und dass Sie wissen müssen, dass es ein continueInneres gibt, um ifzu wissen, dass das breaknicht genommen wird.
Doval
1
Ist es zu früh für Sie, die Ausnahmebehandlung zu lernen?
Mawg sagt, Monica am
2
@Dunk Sie können sofort wissen , was bestimmt , aber Sie werden anhalten und nachdenken müssen härter überprüfen, ob es richtig , was schlimmer ist. dataIsValidist veränderlich. Um zu wissen, dass die Schleife korrekt beendet wird, muss ich überprüfen, ob sie gesetzt ist, wenn die Daten gültig sind, und ob sie zu keinem Zeitpunkt danach deaktiviert wird . In einer Schleife der Form do { ... if (!expr) { break; } ... } while (true);weiß ich, dass bei einer exprAuswertung falsedie Schleife unterbrochen wird , ohne dass der Rest des Schleifenkörpers betrachtet werden muss.
Doval
1
@Doval: Ich weiß auch nicht, wie Sie Code lesen, aber ich lese sicherlich nicht jede Zeile, wenn ich herausfinden möchte, was es tut oder wo der Fehler sein könnte, den ich suche. Wenn ich die while-Schleife gesehen habe, ist (aus meinem Beispiel) sofort ersichtlich, dass die Schleife gültige Daten erhält. Es ist mir egal, wie es funktioniert, ich kann alles andere in der Schleife überspringen, es sei denn, es kommt einfach so vor, dass ich aus irgendeinem Grund ungültige Daten erhalte. Dann würde ich mir den Code in der Schleife ansehen. Ihre Idee erfordert, dass ich mir den gesamten Code in der Schleife ansehe, um herauszufinden, dass er für mich nicht von Interesse ist.
Dunk

Antworten:

15

Ich würde die if-Anweisung etwas anders schreiben, also wird sie genommen, wenn die Eingabe erfolgreich ist.

for (;;) {
    cout << ": ";
    if (cin >> input)
        break;
    cin.clear();
    cin.ignore(512, '\n');
}

Es ist auch kürzer.

Was auf einen kürzeren Weg hindeutet, der Ihrem Lehrer gefallen könnte:

cout << ": ";
while (!(cin >> input)) {
    cin.clear();
    cin.ignore(512, '\n');
    cout << ": ";
}
Sjoerd
quelle
Gute Idee, die Bedingungen so umzukehren, damit ich breakam Ende keine benötige . Das allein macht den Code viel einfacher zu lesen. Ich wusste auch nicht, dass !(cin >> input)das gültig ist. Danke für die Information!
Wefwefa3
8
Das Problem bei der zweiten Lösung besteht darin, dass die Zeile dupliziert wird cout << ": ";, wodurch das DRY-Prinzip verletzt wird. Für Code aus der realen Welt wäre dies ein No-Go, da er fehleranfällig wird (wenn Sie diesen Teil später ändern müssen, müssen Sie sicherstellen, dass Sie nicht vergessen, jetzt zwei Zeilen auf ähnliche Weise zu ändern). Trotzdem habe ich dir für deine erste Version eine +1 gegeben, was meiner Meinung nach ein Beispiel für die breakrichtige Verwendung ist.
Doc Brown
2
Es fühlt sich so an, als würden Sie an Haaren herumhacken, wenn man bedenkt, dass die duplizierte Linie offensichtlich ein bisschen bescheiden ist. Sie könnten die erste cout << ": "vollständig entfernen und nichts würde brechen.
Jdevlin
2
@codingthewheel: Sie haben Recht, dieses Beispiel ist zu trivial, um die potenziellen Risiken beim Ignorieren des DRY-Preises aufzuzeigen. Aber stellen Sie sich eine solche Situation im Code der realen Welt vor, in der das versehentliche Einführen von Fehlern echte Konsequenzen haben kann - dann könnten Sie anders darüber nachdenken. Deshalb habe ich mir nach drei Jahrzehnten der Programmierung angewöhnt, ein solches Loop-Exit-Problem niemals durch Duplizieren des ersten Teils zu lösen, nur "weil ein whileBefehl besser aussieht".
Doc Brown
1
@DocBrown Die Vervielfältigung des Cout ist meist ein Artefakt des Beispiels. In der Praxis unterscheiden sich die beiden cout-Anweisungen, da die zweite eine Fehlermeldung enthält. ZB "Bitte <foo> eingeben:" und "Fehler! Bitte <foo> erneut eingeben:" .
Sjoerd
15

Was Sie anstreben müssen, ist die Vermeidung von Rohschleifen .

Verschieben Sie die komplexe Logik in eine Hilfsfunktion, und plötzlich sind die Dinge viel klarer:

bool getValidUserInput(string & input)
{
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        return false;
    }
    return true;
}

int main() {
    string input;
    while (!getValidUserInput(input)) { 
        // We wait for a valid user input...
    }
}
glampert
quelle
1
Das ist so ziemlich das, was ich vorschlagen wollte. Refactor es, um das Abrufen der Eingabe von der Entscheidung zum Abrufen der Eingabe zu trennen.
Rob K
4
Ich stimme zu, dass dies eine schöne Lösung ist. Es wird zur weit überlegenen Alternative, wenn getValidUserInputes mehrmals und nicht nur einmal aufgerufen wird. Ich gebe dir +1 dafür, aber ich werde stattdessen Sjoerds Antwort akzeptieren, weil ich finde, dass sie meine Frage besser beantwortet.
Wefwefa3
@ user3787875: genau das, was ich dachte, als ich die beiden Antworten las - gute Wahl.
Doc Brown
Dies ist der logische nächste Schritt nach dem Umschreiben in meiner Antwort.
Sjoerd
9

Es ist nicht so sehr for(;;)schlecht. Es ist einfach nicht so klar wie Muster wie:

while (cin.fail()) {
    ...
}

Oder wie Sjoerd es ausdrückte:

while (!(cin >> input)) {
    ...
}

Betrachten wir das primäre Publikum für dieses Zeug als Ihre Programmierkollegen, einschließlich der zukünftigen Version von Ihnen, die sich nicht mehr daran erinnert, warum Sie die Pause am Ende so durchgehalten oder die Pause mit dem Fortfahren übersprungen haben. Dieses Muster aus Ihrem Beispiel:

for (;;) {
    ...
    if (blah) {
        continue;
    }
    break;
}

... erfordert ein paar Sekunden zusätzlichen Denkens, um im Vergleich zu anderen Mustern zu simulieren. Es ist keine feste Regel, aber das Überspringen der break-Anweisung mit dem Fortsetzen fühlt sich chaotisch oder klug an, ohne nützlich zu sein, und das Setzen der break-Anweisung am Ende der Schleife, selbst wenn es funktioniert, ist ungewöhnlich. Normalerweise werden beide breakund continueverwendet, um die Schleife oder diese Iteration der Schleife vorzeitig zu evakuieren, sodass es sich etwas seltsam anfühlt, wenn Sie am Ende eine der beiden Schleifen sehen, auch wenn dies nicht der Fall ist.

Davon abgesehen gute Sachen!

jdevlin
quelle
Ihre Antwort sieht gut aus - aber nur auf den ersten Blick. Tatsächlich verbirgt es das inhärente Problem, das wir in @Sjoerd sehen können, s answer: using a während die Schleife auf diese Weise zu unnötiger Codeduplizierung für das gegebene Beispiel führen kann. Und das ist ein Problem, das meiner Meinung nach mindestens genauso wichtig ist wie die allgemeine Lesbarkeit der Schleife.
Doc Brown
Entspann dich, Doc. OP bat um einige grundlegende Ratschläge / Rückmeldungen zu seiner Schleifenstruktur, ich gab ihm die Standardinterpretation. Sie können für andere Interpretationen argumentieren - der Standardansatz ist nicht immer der beste oder mathematisch strengste Ansatz -, aber insbesondere, da sein eigener Ausbilder sich auf die Verwendung von stützte while, halte ich das Bit "nur auf den ersten Blick" für ungerechtfertigt .
Jdevlin
Versteh mich nicht falsch, das hat nichts damit zu tun, "mathematisch streng" zu sein - hier geht es darum, entwicklungsfähigen Code zu schreiben - Code, bei dem das Hinzufügen neuer Funktionen oder das Ändern vorhandener Funktionen das geringstmögliche Risiko für die Einführung von Fehlern birgt . In meinem Beruf ist das wirklich kein akademisches Problem.
Doc Brown
Ich möchte hinzufügen, dass ich Ihrer Antwort größtenteils zustimme. Ich wollte nur darauf hinweisen, dass Ihre Vereinfachung am Anfang ein häufiges Problem verbergen könnte . Wenn ich die Möglichkeit habe, "while" ohne Code-Duplizierung zu verwenden, werde ich nicht zögern, es zu verwenden.
Doc Brown
1
Einverstanden und manchmal ein oder zwei unnötige Zeilen vor der Schleife ist der Preis, den wir für diese deklarative frontgeladene whileSyntax zahlen . Ich mag es nicht, etwas vor der Schleife zu tun, die auch innerhalb der Schleife ausgeführt wird. Andererseits mag ich auch keine Schleife, die sich in Knoten bindet und versucht, das Präfix umzugestalten. So oder so ein Balanceakt.
Jdevlin
3

Mein Logiklehrer in der Schule sagte immer und schlug mir ins Gehirn, dass es nur einen Eingang und einen Ausgang für Schleifen geben sollte. Wenn nicht, erhalten Sie Spaghetti-Code und wissen nicht, wohin der Code während des Debuggens geht.

Im wirklichen Leben denke ich, dass die Lesbarkeit von Code wirklich wichtig ist, damit jemand anderes ein Problem mit Ihrem Code beheben oder debuggen muss, was für ihn einfach ist.

Wenn es sich um ein Leistungsproblem handelt, weil Sie diesen Look millionenfach durchlaufen, ist die for-Schleife möglicherweise schneller. Testen würde diese Frage beantworten.

Ich kenne C ++ nicht, aber ich habe die ersten beiden Ihrer Codebeispiele vollständig verstanden. Ich gehe davon aus, dass continue bis zum Ende der for-Schleife geht und diese dann erneut wiederholt. Das while-Beispiel habe ich vollständig verstanden, aber für mich war es einfacher zu verstehen als Ihr for (;;) - Beispiel. Beim dritten musste ich einige Gedanken machen, um es herauszufinden.

Also ging für mich vorbei, was einfacher zu lesen war (ohne C ++ zu kennen) und was mein Logiklehrer sagte, ich würde die while-Schleife eins verwenden.

Jaydel Gluckie
quelle
4
Ihr Lehrer ist also gegen die Verwendung von Ausnahmen? Und vorzeitige Rückkehr? Und ähnliche Dinge? Richtig eingesetzt, machen sie alle Code lesbarer ...
Deduplicator
1
Der Schlüsselbegriff in Ihrem ellipsierten Satz lautet "richtig eingesetzt". Meine bittere Erfahrung scheint mir darauf hinzudeuten, dass verdammt nahe NIEMAND in diesem verrückten Schläger weiß, wie man diese Dinge "richtig" einsetzt, für fast jede vernünftige Definition von "richtig" (oder "lesbar").
John R. Strohm
2
Denken Sie daran, dass es sich um eine Klasse handelt, in der die Schüler Anfänger sind. Es ist sicherlich eine gute Idee, sie zu trainieren, um diese Dinge zu vermeiden, die als Anfänger häufig auftreten (bevor sie die Auswirkungen verstehen und über eine Ausnahme von der Regel nachdenken können).
user1118321
2
Das Dogma "Ein Eingang - einmal Ausgang" ist meiner Meinung nach eine antiquierte Sichtweise, die wahrscheinlich von Edward Dijkstra zu einer Zeit "erfunden" wurde, als Leute mit GOTOs in Sprachen wie Basic, Pascal und COBOL herumgespielt haben. Ich stimme voll und ganz dem zu, was Deduplicator oben geschrieben hat. Darüber hinaus enthalten die ersten beiden Beispiele des OP tatsächlich nur einen Eintrag und einen Ausgang, dennoch ist die Lesbarkeit mittelmäßig.
Doc Brown
4
Stimmen Sie mit @DocBrown überein. "Ein Eingang ein Ausgang" in modernen Produktionscodebasen ist in der Regel ein Rezept für Code-Cruft und fördert die tiefe Verschachtelung von Kontrollstrukturen.
Jdevlin
-2

Für mich ist die einfachste C-Übersetzung des Pseudocodes

do
    {
    success = something();
    if (success == FALSE)
        {
        handle_the_error();
        }
    } while (success == FALSE)
\\ moving on ...

Ich verstehe nicht, warum diese offensichtliche Übersetzung ein Problem ist.

vielleicht das:

while (!something())
    {
    handle_the_error();
    }

das sieht einfacher aus.

robert bristow-johnson
quelle