while (true) und loop-breaking - anti-pattern?

32

Betrachten Sie den folgenden Code:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Angenommen, dieser Prozess beinhaltet eine endliche, aber eingabeabhängige Anzahl von Schritten. Die Schleife ist so konzipiert, dass sie aufgrund des Algorithmus von selbst endet und nicht auf unbestimmte Zeit ausgeführt wird (bis sie durch ein externes Ereignis abgebrochen wird). Da sich der Test, ob die Schleife enden soll, in der Mitte einer logischen Reihe von Schritten befindet, überprüft die while-Schleife selbst derzeit nichts Sinnvolles. Die Prüfung wird stattdessen an der "richtigen" Stelle innerhalb des konzeptionellen Algorithmus durchgeführt.

Mir wurde gesagt, dass dies ein fehlerhafter Code ist, da er aufgrund der Endbedingung, die von der Schleifenstruktur nicht überprüft wird, fehleranfälliger ist. Es ist schwieriger herauszufinden, wie Sie die Schleife verlassen würden, und es kann zu Fehlern kommen, da die Unterbrechungsbedingung möglicherweise umgangen oder bei zukünftigen Änderungen versehentlich weggelassen wird.

Der Code könnte nun folgendermaßen aufgebaut sein:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

Dies dupliziert jedoch einen Aufruf einer Methode im Code, wodurch DRY verletzt wird. Wenn TransformInSomeWaysie später durch eine andere Methode ersetzt würden, müssten beide Aufrufe gefunden und geändert werden (und die Tatsache, dass es zwei gibt, ist in einem komplexeren Codeteil möglicherweise weniger offensichtlich).

Sie könnten es auch so schreiben:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

... aber Sie haben jetzt eine Variable, deren einziger Zweck darin besteht, die Bedingungsüberprüfung auf die Schleifenstruktur zu verschieben, und die außerdem mehrmals überprüft werden muss, um dasselbe Verhalten wie die ursprüngliche Logik zu erzielen.

Ich für meinen Teil sage, dass angesichts des Algorithmus, den dieser Code in der realen Welt implementiert, der Originalcode am besten lesbar ist. Wenn Sie es selbst durchgehen würden, würden Sie so darüber nachdenken, und daher wäre es für Leute, die mit dem Algorithmus vertraut sind, intuitiv.

Also, was ist "besser"? Ist es besser, die Zustandsüberprüfung der while-Schleife zu übertragen, indem die Logik um die Schleife herum strukturiert wird? Oder ist es besser, die Logik auf "natürliche" Weise zu strukturieren, wie dies durch Anforderungen oder eine konzeptionelle Beschreibung des Algorithmus angezeigt wird, obwohl dies bedeuten kann, dass die eingebauten Funktionen der Schleife umgangen werden?

KeithS
quelle
12
Möglicherweise, aber trotz der Codebeispiele ist die gestellte Frage IMO konzeptioneller, was eher diesem Bereich der SE entspricht. Was ist ein Muster oder Anti-Muster wird hier oft diskutiert, und das ist die eigentliche Frage; Ist es eine schlechte Sache, eine Schleife so zu strukturieren, dass sie unendlich läuft und dann daraus ausbricht?
KeithS
3
Das do ... untilKonstrukt ist auch für diese Art von Schleifen nützlich, da sie nicht "grundiert" werden müssen.
Blrfl
2
Der Fall mit anderthalb Schlingen fehlt immer noch eine wirklich gute Antwort.
Loren Pechtel
1
"Dies dupliziert jedoch einen Aufruf einer Methode im Code, wodurch DRY verletzt wird." Ich bin mit dieser Behauptung nicht einverstanden und es scheint ein Missverständnis des Prinzips zu sein.
Andy
1
Abgesehen davon, dass ich C-Style für (;;) bevorzuge, was explizit bedeutet, dass ich eine Schleife habe und die Schleifenanweisung nicht einchecke, ist Ihr erster Ansatz absolut richtig. Du hast eine Schleife. Bevor Sie entscheiden können, ob Sie das Programm beenden möchten, müssen Sie noch einige Arbeiten ausführen. Dann beenden Sie, wenn einige Bedingungen erfüllt sind. Dann erledigen Sie die eigentliche Arbeit und fangen von vorne an. Das ist absolut in Ordnung, leicht zu verstehen, logisch, nicht redundant und leicht zu korrigieren. Die zweite Variante ist einfach schrecklich, während die dritte nur sinnlos ist; schrecklich drehen, wenn der Rest der Schleife, die in das if geht, sehr lang ist.
gnasher729

Antworten:

14

Bleiben Sie bei Ihrer ersten Lösung. Schleifen können sehr kompliziert werden und eine oder mehrere saubere Unterbrechungen können für Sie, jeden anderen, der sich Ihren Code, das Optimierungsprogramm und die Leistung des Programms ansieht, viel einfacher sein als aufwändige if-Anweisungen und hinzugefügte Variablen. Mein üblicher Ansatz ist es, eine Schleife mit so etwas wie "while (true)" zu erstellen und die bestmögliche Codierung zu erhalten. Oft kann und kann ich dann die Unterbrechung (en) durch eine Standardschleifensteuerung ersetzen; Schließlich sind die meisten Loops ziemlich einfach. Die Endbedingung sollte durch die Schleifenstruktur für die Gründe überprüft werden Sie erwähnen, aber nicht auf Kosten der Zugabe von ganz neuen Variablen, Hinzufügen if-Anweisungen und sich wiederholende Code, alle sind noch mehr Fehler anfällig.

RalphChapin
quelle
"if-Anweisungen und sich wiederholender Code, die alle noch fehleranfälliger sind." - Ja, wenn ich Leute versuche, nenne ich "Future Bugs"
Pat
13

Es ist nur dann ein bedeutungsvolles Problem, wenn der Schleifenkörper nicht trivial ist. Wenn der Körper so klein ist, ist die Analyse so trivial und überhaupt kein Problem.

DeadMG
quelle
7

Ich habe Probleme, die anderen Lösungen besser zu finden als die mit Pause. Nun, ich bevorzuge den Ada-Weg, der das erlaubt

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

aber die break-lösung ist das sauberste rendern.

Mein POV ist, dass, wenn eine Kontrollstruktur fehlt, die sauberste Lösung darin besteht, sie mit anderen Kontrollstrukturen zu emulieren, ohne den Datenfluss zu verwenden. (Und in ähnlicher Weise, wenn ich ein Datenflussproblem habe und reine Datenflusslösungen gegenüber einer Lösung mit Kontrollstrukturen vorziehen möchte *).

Es ist schwieriger herauszufinden, wie Sie die Schleife verlassen würden,

Verwechseln Sie sich nicht, die Diskussion würde nicht stattfinden, wenn die Schwierigkeit nicht zum größten Teil gleich wäre: Der Zustand ist derselbe und am selben Ort vorhanden.

Welche von

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

und

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

die beabsichtigte Struktur besser machen? Ich stimme für die erste, Sie müssen nicht überprüfen, ob die Bedingungen übereinstimmen. (Aber siehe meine Einrückung).

Ein Programmierer
quelle
1
Oh, sieh mal, eine Sprache, die Mid-Exit-Schleifen direkt unterstützt! :)
mjfgates
Ich mag Ihren Standpunkt, fehlende Kontrollstrukturen mit Kontrollstrukturen zu emulieren. Das ist wahrscheinlich auch eine gute Antwort auf GOTO-bezogene Fragen. Die Kontrollstrukturen einer Sprache sollten immer dann verwendet werden, wenn sie den semantischen Anforderungen entsprechen. Wenn ein Algorithmus jedoch etwas anderes erfordert, sollte die vom Algorithmus geforderte Kontrollstruktur verwendet werden.
Supercat
3

while (wahr) und break ist eine verbreitete Redewendung. Dies ist die kanonische Methode, um Mid-Exit-Schleifen in C-ähnlichen Sprachen zu implementieren. Das Hinzufügen einer Variablen zum Speichern der Ausgangsbedingung und eines if () in der zweiten Hälfte der Schleife ist eine sinnvolle Alternative. Einige Läden mögen die eine oder andere offiziell standardisieren, aber keine ist überlegen; sie sind gleichwertig.

Ein guter Programmierer, der in diesen Sprachen arbeitet, sollte wissen können, was auf einen Blick vor sich geht, wenn er eine der beiden Sprachen sieht. Es könnte eine gute kleine Interviewfrage sein; Hand jemand zwei Schleifen, eine mit jeder Redewendung, und frage ihn, was der Unterschied ist (richtige Antwort: "nichts", mit vielleicht einem Zusatz von "aber ich benutze gewohnheitsmäßig DIESE.")

mjfgates
quelle
2

Ihre Alternativen sind nicht so schlecht, wie Sie vielleicht denken. Guter Code sollte:

  1. Geben Sie mit guten Variablennamen / Kommentaren deutlich an, unter welchen Bedingungen die Schleife beendet werden soll. (Die Bruchbedingung sollte nicht verborgen sein)

  2. Geben Sie klar an, in welcher Reihenfolge die Operationen ausgeführt werden sollen. Hier ist DoSomethingElseTo(input)es eine gute Idee , die optionale dritte Operation ( ) in das if zu setzen.

Das heißt, es ist leicht, perfektionistische, messingpolierende Instinkte viel Zeit in Anspruch nehmen zu lassen. Ich würde nur die Einstellungen anpassen, wenn dies oben erwähnt wurde. Kommentieren Sie den Code und fahren Sie fort.

Klopfen
quelle
Ich denke, perfektionistische, messingpolierende Instinkte sparen auf lange Sicht Zeit. Hoffentlich entwickeln Sie mit der Zeit solche Gewohnheiten, sodass Ihr Code perfekt ist und Messing poliert wird, ohne viel Zeit zu verbrauchen. Im Gegensatz zu physischen Konstruktionen kann Software jedoch für immer Bestand haben und an unendlich vielen Orten eingesetzt werden. Die Einsparungen durch Code, der sogar 0,00000001% schneller, zuverlässiger, verwendbarer und wartbarer ist, können erheblich sein. (Natürlich ist der Großteil meines hochglanzpolierten Codes in Sprachen längst überholt oder es hat mir geholfen, gute Gewohnheiten zu entwickeln.)
RalphChapin
Naja ich kann dich nicht falsch nennen :-) Dies ist eine Meinungsfrage und wenn gute Fähigkeiten beim Messingpolieren herausgekommen sind: super.
Pat
Ich denke, es ist trotzdem eine Möglichkeit, darüber nachzudenken. Ich würde sicherlich keine Garantien anbieten wollen. Ich fühle mich besser, wenn ich denke, dass mein Messingpolieren meine Fähigkeiten verbessert hat, so dass ich in diesem Punkt möglicherweise Vorurteile habe.
RalphChapin
2

Die Leute sagen, es sei eine schlechte Übung while (true), und die meiste Zeit haben sie Recht. Wenn Ihre whileAnweisung viele komplizierte Codes enthält und es unklar ist, wann Sie aus if ausbrechen, bleibt Ihnen schwer zu pflegender Code und ein einfacher Fehler kann eine Endlosschleife verursachen.

In Ihrem Beispiel ist die Verwendung korrekt, while(true)da Ihr Code klar und leicht verständlich ist. Es hat keinen Sinn, ineffizienten und schwerer lesbaren Code zu erstellen, nur weil manche Leute es für immer schlecht halten, ihn zu verwenden while(true).

Ich hatte ein ähnliches Problem:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

Durch do ... while(true)die Verwendung von wird vermieden, dass isset($array][$key]unnötigerweise zweimal aufgerufen wird. Der Code ist kürzer, übersichtlicher und einfacher zu lesen. Es besteht absolut keine Gefahr, dass else break;am Ende ein Endlosschleifen-Fehler auftritt .

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

Es ist gut, guten Praktiken zu folgen und schlechte Praktiken zu vermeiden , aber es gibt immer Ausnahmen, und es ist wichtig, offen zu bleiben und diese Ausnahmen zu erkennen.

Dan Bray
quelle
0

Wenn ein bestimmter Steuerungsfluss auf natürliche Weise als break-Anweisung ausgedrückt wird, ist es grundsätzlich nie besser, andere Flusssteuerungsmechanismen zum Emulieren einer break-Anweisung zu verwenden.

(Beachten Sie, dass dies das teilweise Abrollen der Schleife und das Herunterrutschen des Schleifenkörpers umfasst, so dass der Beginn der Schleife mit dem bedingten Test übereinstimmt.)

Wenn Sie Ihre Schleife so reorganisieren können, dass der Kontrollfluss natürlich durch eine bedingte Prüfung am Anfang der Schleife ausgedrückt wird, ist das großartig. Es könnte sich sogar lohnen, sich Gedanken darüber zu machen, ob dies möglich ist. Versuchen Sie aber nicht, einen quadratischen Stift in ein rundes Loch zu stecken.


quelle
0

Sie könnten auch damit gehen:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

Oder weniger verwirrend:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}
Finsternis
quelle
1
Während einige Leute es mögen, behaupte ich, dass die Schleifenbedingung im Allgemeinen für bedingte Tests reserviert sein sollte, anstatt für Anweisungen, die Dinge tun.
5
Komma-Ausdruck in einer Schleifenbedingung ... Das ist schrecklich. Du bist zu schlau. Und dies funktioniert nur in diesem einfachen Fall, in dem TransformInSomeWay als Ausdruck ausgedrückt werden kann.
gnasher729
0

Wenn die Komplexität der Logik unhandlich wird, ist die Verwendung einer unbegrenzten Schleife mit Unterbrechungen in der Mitte der Schleife zur Flusssteuerung tatsächlich am sinnvollsten. Ich habe ein Projekt mit einem Code, der wie folgt aussieht. (Beachten Sie die Ausnahme am Ende der Schleife.)

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

Um diese Logik ohne uneingeschränkte Unterbrechung der Schleife auszuführen, würde ich am Ende etwa Folgendes erhalten:

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

Die Ausnahme wird jetzt in einer Hilfsmethode ausgelöst. Es hat auch ziemlich viel if ... elseNesting. Ich bin mit diesem Ansatz nicht zufrieden. Daher finde ich das erste Muster am besten.

unerschrocken
quelle
Tatsächlich habe ich diese Frage auf SO gestellt und wurde in Flammen niedergeschossen ... stackoverflow.com/questions/47253486/…
intrepidis