Wie vermeide ich Wenn-Ketten?


266

Angenommen, ich habe diesen Pseudocode:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

Funktionen executeStepXsollten genau dann ausgeführt werden, wenn die vorherigen erfolgreich sind. In jedem Fall sollte die executeThisFunctionInAnyCaseFunktion am Ende aufgerufen werden. Ich bin ein Neuling in der Programmierung, also entschuldige die sehr grundlegende Frage: Gibt es eine Möglichkeit (zum Beispiel in C / C ++) zu vermeiden, dass diese lange ifKette diese Art von "Codepyramide" auf Kosten der Lesbarkeit des Codes erzeugt? ?

Ich weiß, wenn wir den executeThisFunctionInAnyCaseFunktionsaufruf überspringen könnten , könnte der Code wie folgt vereinfacht werden:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Die Einschränkung ist jedoch der executeThisFunctionInAnyCaseFunktionsaufruf. Könnte die breakAussage auf irgendeine Weise verwendet werden?


254
@ FrédéricHamidi falsch falsch falsch! Sagen Sie niemals, dass es gut ist, Ihren Programmfluss mit Ausnahmen zu steuern! Ausnahmen sind aus zu vielen Gründen definitiv NICHT für diesen Zweck geeignet.
Piotr Zierhoffer

26
@Piotr, ich wurde von Python verwöhnt (was dies tatsächlich fördert). Ich weiß, dass Ausnahmen nicht für die Flusskontrolle in C ++ verwendet werden sollen, aber ist es hier wirklich Flusskontrolle? Könnte eine zurückkehrende Funktion falsenicht als einer Ausnahmesituation ähnlich angesehen werden?
Frédéric Hamidi

13
Das hängt von der Semantik eines Programms ab. Eine falseRückkehr kann ziemlich normal sein.
Dornhege

28
Ich habe Ihre Frage auf die erste Überarbeitung zurückgesetzt. Sie sollten Ihre Frage nicht radikal ändern, nachdem Sie eine bestimmte Anzahl von Fragen (> 0) erhalten haben, da dies alle bis zu diesem Moment gegebenen Antworten ungültig macht und Verwirrung stiftet. Öffnen Sie stattdessen eine neue Frage.
Schuh

13
Ich wünschte, alle "Programmieranfänger" würden solche Designfragen stellen.
Jezen Thomas

Antworten:


486

Sie können ein &&(logisches UND) verwenden:

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

Dies wird Ihre beiden Anforderungen erfüllen:

  • executeStep<X>()sollte nur ausgewertet werden, wenn die vorherige erfolgreich war (dies wird als Kurzschlussauswertung bezeichnet )
  • executeThisFunctionInAnyCase() wird in jedem Fall ausgeführt

29
Dies ist sowohl semantisch korrekt (tatsächlich möchten wir, dass ALLE Bedingungen erfüllt sind) als auch eine sehr gute Programmiertechnik (Kurzschlussbewertung). Darüber hinaus kann dies in jeder komplexen Situation verwendet werden, in der das Erstellen von Funktionen den Code durcheinander bringen würde.
Sanchises

58
@RobAu: Dann ist es gut für die Junior-Programmierer, endlich Code zu sehen, der auf einer kurzen Bewertung beruht, und sie möglicherweise dazu aufzufordern, dieses Thema erneut zu untersuchen, was ihnen wiederum auf ihrem Weg helfen würde, schließlich Senior-Programmierer zu werden. Also eindeutig eine Win-Win-Situation: Anständiger Code und jemand hat etwas aus dem Lesen gelernt.
x4u

24
Dies sollte die beste Antwort sein
Anzeigename

61
@RobAu: Dies ist kein Hack, der einen obskuren Syntaxtrick ausnutzt. Er ist in fast jeder Programmiersprache sehr idiomatisch und bis zu einer unbestreitbaren Standardpraxis.
BlueRaja - Danny Pflughoeft

38
Diese Lösung gilt nur, wenn die Bedingungen tatsächlich so einfache Funktionsaufrufe sind. Im realen Code können diese Bedingungen 2-5 Zeilen lang sein (und selbst Kombinationen aus vielen anderen &&und ||), sodass Sie sie auf keinen Fall zu einer einzigen ifAnweisung zusammenfügen möchten, ohne die Lesbarkeit zu beeinträchtigen. Und es ist nicht immer einfach, diese Bedingungen auf äußere Funktionen zu verschieben, da sie möglicherweise von vielen zuvor berechneten lokalen Variablen abhängen, was zu einem schrecklichen Durcheinander führen würde, wenn Sie versuchen, jede einzelne als einzelnes Argument zu übergeben.
Hamstergen

358

Verwenden Sie einfach eine zusätzliche Funktion, damit Ihre zweite Version funktioniert:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

Wenn Sie entweder tief verschachtelte ifs (Ihre erste Variante) oder den Wunsch verwenden, aus einem "Teil einer Funktion" auszubrechen, benötigen Sie normalerweise eine zusätzliche Funktion.


51
+1 endlich hat jemand eine vernünftige Antwort gepostet. Dies ist meiner Meinung nach der korrekteste, sicherste und lesbarste Weg.
Lundin

31
+1 Dies ist ein gutes Beispiel für das "Prinzip der Einzelverantwortung". Die Funktion fooarbeitet sich durch eine Abfolge verwandter Bedingungen und Aktionen. Die Funktion barist sauber von den Entscheidungen getrennt. Wenn wir die Details der Bedingungen und Maßnahmen sehen würden, könnte sich herausstellen, dass dies fooimmer noch zu viel ist, aber im Moment ist dies eine gute Lösung.
GraniteRobert

13
Der Nachteil ist, dass C keine verschachtelten Funktionen hat. Wenn diese drei Schritte zur Verwendung von Variablen von barIhnen erforderlich sind, müssen Sie sie manuell fooals Parameter übergeben. Wenn dies der Fall ist und foonur einmal aufgerufen wird, würde ich mich irren, die goto-Version zu verwenden, um zu vermeiden, dass zwei eng gekoppelte Funktionen definiert werden, die am Ende nicht sehr wiederverwendbar wären.
Hugomg

7
Ich bin mir nicht sicher über die Kurzschlusssyntax für C, aber in C # könnte foo () geschrieben werden alsif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Gehe zu werden ständig verwendet, insbesondere bei der Systemprogrammierung, wenn Sie viel Setup-Code abwickeln müssen.
Scotty Bauer

166

gotoIn diesem Fall verwenden C-Programmierer der alten Schule . Es ist die einzige Verwendung goto, die vom Linux-Styleguide tatsächlich empfohlen wird. Sie wird als zentraler Funktions-Exit bezeichnet:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Einige Leute arbeiten daran, gotoindem sie den Körper in eine Schleife wickeln und daraus abbrechen, aber effektiv tun beide Ansätze dasselbe. Der gotoAnsatz ist besser, wenn Sie nur dann eine andere Bereinigung benötigen, wenn dies executeStepA()erfolgreich war:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

Mit dem Loop-Ansatz würden Sie in diesem Fall zwei Ebenen von Loops erhalten.


108
+1. Viele Leute sehen einen gotound denken sofort "Das ist schrecklicher Code", aber er hat seine gültigen Verwendungen.
Graeme Perrow

46
Abgesehen davon, dass dies aus Sicht der Wartung ein ziemlich unordentlicher Code ist. Besonders wenn es mehrere Beschriftungen gibt, bei denen der Code auch schwerer zu lesen ist. Es gibt elegantere Möglichkeiten, dies zu erreichen: Verwenden Sie Funktionen.
Lundin

29
-1 Ich sehe das gotound ich muss nicht einmal denken, um zu sehen, dass dies schrecklicher Code ist. Ich musste einmal so etwas pflegen, es ist sehr böse. Das OP schlägt am Ende der Frage eine vernünftige Alternative in C-Sprache vor, die ich in meine Antwort aufgenommen habe.
Prost und hth. - Alf

56
An dieser begrenzten, in sich geschlossenen Verwendung von goto ist nichts auszusetzen. Aber Vorsicht, goto ist eine Einstiegsdroge, und wenn Sie nicht aufpassen, werden Sie eines Tages feststellen, dass niemand sonst Spaghetti mit den Füßen isst, und Sie tun dies seit 15 Jahren, nachdem Sie gedacht haben: "Ich kann das einfach beheben." sonst albtraumhafter Fehler mit einem Etikett ... "
Tim Post

66
Ich habe wahrscheinlich hunderttausend Zeilen extrem klaren, leicht zu pflegenden Codes in diesem Stil geschrieben. Die beiden wichtigsten Dinge, die zu verstehen sind, sind (1) Disziplin! Legen Sie eine klare Richtlinie für das Layout jeder Funktion fest und verletzen Sie diese nur bei äußerster Notwendigkeit. (2) Verstehen Sie, dass wir hier Ausnahmen in einer Sprache simulieren, in der sie nicht vorhanden sind . throwist in vielerlei Hinsicht schlimmer als gotoweil throwes nicht einmal aus dem lokalen Kontext klar ist, wo Sie am Ende landen werden! Verwenden Sie für den Kontrollfluss im goto-Stil dieselben Entwurfsüberlegungen wie für Ausnahmen.
Eric Lippert

131

Dies ist eine häufige Situation, und es gibt viele gängige Möglichkeiten, damit umzugehen. Hier ist mein Versuch einer kanonischen Antwort. Bitte kommentieren Sie, wenn ich etwas verpasst habe und ich werde diesen Beitrag auf dem neuesten Stand halten.

Dies ist ein Pfeil

Was Sie besprechen, wird als Pfeil-Anti-Muster bezeichnet . Es wird als Pfeil bezeichnet, da die Kette verschachtelter ifs Codeblöcke bildet, die sich immer weiter nach rechts und dann wieder nach links ausdehnen und einen visuellen Pfeil bilden, der auf die rechte Seite des Code-Editor-Bereichs "zeigt".

Flache den Pfeil mit der Wache ab

Einige gebräuchliche Methoden zur Vermeidung des Pfeils werden hier erläutert . Die gängigste Methode ist die Verwendung eines Wachmusters, in dem der Code behandelt die Ausnahme erste fließt und dann übernimmt die Grundströmung, beispielsweise anstelle von

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... du würdest benutzen ....

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

Wenn es eine lange Reihe von Wachen gibt, wird der Code dadurch erheblich abgeflacht, da alle Wachen ganz links angezeigt werden und Ihre Wenns nicht verschachtelt sind. Darüber hinaus koppeln Sie die logische Bedingung visuell mit dem zugehörigen Fehler, wodurch Sie viel einfacher erkennen können, was gerade passiert:

Pfeil:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

Bewachen:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

Dies ist objektiv und quantifizierbar leichter zu lesen, weil

  1. Die Zeichen {und} für einen bestimmten Logikblock liegen näher beieinander
  2. Der mentale Kontext, der zum Verstehen einer bestimmten Linie benötigt wird, ist geringer
  3. Die gesamte Logik, die einer if-Bedingung zugeordnet ist, befindet sich eher auf einer Seite
  4. Die Notwendigkeit, dass der Codierer die Seiten- / Augenspur scrollt, wird erheblich verringert

So fügen Sie am Ende allgemeinen Code hinzu

Das Problem mit dem Schutzmuster besteht darin, dass es auf dem beruht, was als "opportunistische Rückkehr" oder "opportunistischer Ausstieg" bezeichnet wird. Mit anderen Worten, es bricht das Muster, dass jede Funktion genau einen Austrittspunkt haben sollte. Dies ist aus zwei Gründen ein Problem:

  1. Es reibt einige Leute in die falsche Richtung, z. B. Leute, die gelernt haben, auf Pascal zu programmieren, haben gelernt, dass eine Funktion = ein Austrittspunkt ist.
  2. Es enthält keinen Codeabschnitt, der beim Beenden ausgeführt wird, unabhängig davon , um welches Thema es sich handelt.

Im Folgenden habe ich einige Optionen bereitgestellt, um diese Einschränkung zu umgehen, indem ich entweder Sprachfunktionen verwende oder das Problem insgesamt vermeide.

Option 1. Sie können dies nicht tun: verwenden finally

Als C ++ - Entwickler können Sie dies leider nicht tun. Dies ist jedoch die Antwort Nummer eins für Sprachen, die ein Schlüsselwort finally enthalten, da dies genau das ist, wofür es ist.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

Option 2. Vermeiden Sie das Problem: Restrukturieren Sie Ihre Funktionen

Sie können das Problem vermeiden, indem Sie den Code in zwei Funktionen aufteilen. Diese Lösung hat den Vorteil, dass sie für jede Sprache funktioniert. Darüber hinaus kann sie die zyklomatische Komplexität reduzieren. Dies ist eine bewährte Methode zur Reduzierung Ihrer Fehlerrate und verbessert die Spezifität automatisierter Komponententests.

Hier ist ein Beispiel:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Option 3. Sprachtrick: Verwenden Sie eine gefälschte Schleife

Ein weiterer häufiger Trick, den ich sehe, ist die Verwendung von while (true) und break, wie in den anderen Antworten gezeigt.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

Dies ist zwar weniger "ehrlich" als die Verwendung goto, aber es ist weniger anfällig, beim Refactoring durcheinander zu kommen, da es die Grenzen des logischen Umfangs klar markiert. Ein naiver Codierer, der Ihre Etiketten oder gotoAussagen ausschneidet und einfügt, kann große Probleme verursachen! (Und ehrlich gesagt ist das Muster jetzt so verbreitet, dass ich denke, es kommuniziert klar die Absicht und ist daher überhaupt nicht "unehrlich").

Es gibt andere Varianten dieser Optionen. Zum Beispiel könnte man switchanstelle von verwenden while. Jedes Sprachkonstrukt mit einem breakSchlüsselwort würde wahrscheinlich funktionieren.

Option 4. Nutzen Sie den Objektlebenszyklus

Ein anderer Ansatz nutzt den Objektlebenszyklus. Verwenden Sie ein Kontextobjekt, um Ihre Parameter zu übertragen (etwas, das unserem naiven Beispiel verdächtig fehlt), und entsorgen Sie es, wenn Sie fertig sind.

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Hinweis: Stellen Sie sicher, dass Sie den Objektlebenszyklus der Sprache Ihrer Wahl verstehen. Damit dies funktioniert, benötigen Sie eine Art deterministische Garbage Collection, dh Sie müssen wissen, wann der Destruktor aufgerufen wird. In einigen Sprachen müssen Sie Disposeanstelle eines Destruktors verwenden.

Option 4.1. Nutzen Sie den Objektlebenszyklus (Wrapper-Muster)

Wenn Sie einen objektorientierten Ansatz verwenden möchten, können Sie dies auch richtig machen. Diese Option verwendet eine Klasse, um die zu bereinigenden Ressourcen sowie die anderen Vorgänge zu "verpacken".

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Stellen Sie erneut sicher, dass Sie Ihren Objektlebenszyklus verstehen.

Option 5. Sprachtrick: Verwenden Sie die Kurzschlussauswertung

Eine andere Technik besteht darin, die Kurzschlussbewertung zu nutzen .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

Diese Lösung nutzt die Arbeitsweise des && Operators. Wenn die linke Seite von && als falsch ausgewertet wird, wird die rechte Seite niemals ausgewertet.

Dieser Trick ist am nützlichsten, wenn kompakter Code erforderlich ist und wenn für den Code wahrscheinlich keine große Wartung erforderlich ist, z. B. wenn Sie einen bekannten Algorithmus implementieren. Für eine allgemeinere Codierung ist die Struktur dieses Codes zu spröde. Selbst eine geringfügige Änderung der Logik kann ein vollständiges Umschreiben auslösen.


12
Schließlich? C ++ hat keine finally-Klausel. Ein Objekt mit einem Destruktor wird in Situationen verwendet, in denen Sie glauben, dass Sie eine finally-Klausel benötigen.
Bill Door

1
Bearbeitet, um die beiden obigen Kommentare zu berücksichtigen.
John Wu

2
Mit trivialem Code (z. B. in meinem Beispiel) sind verschachtelte Muster wahrscheinlich verständlicher. Mit realem Code (der mehrere Seiten umfassen kann) ist das Schutzmuster objektiv leichter zu lesen, da weniger Scrollen und weniger Eye-Tracking erforderlich sind, z. B. ist der durchschnittliche Abstand von {zu} kürzer.
John Wu

1
Ich habe ein verschachteltes Muster gesehen, bei dem der Code auf einem 1920 x 1080-Bildschirm nicht mehr sichtbar war. Versuchen Sie herauszufinden, welcher Fehlerbehandlungscode ausgeführt wird, wenn die dritte Aktion fehlschlägt. Ich habe do {...} verwendet, während (0) stattdessen, damit Sie nicht die letzte Pause brauchen (andererseits, während (true) {...} "Weiter" von
vorne

2
Ihre Option 4 ist tatsächlich ein Speicherverlust in C ++ (ohne Berücksichtigung des geringfügigen Syntaxfehlers). In solchen Fällen wird "new" nicht verwendet, sondern nur "MyContext myContext;".
Sumudu Fernando

60

Mach einfach

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

So einfach ist das.


Aufgrund von drei Änderungen, durch die jede die Frage grundlegend geändert hat (vier, wenn man die Revision auf Version 1 zurückzählt), füge ich das Codebeispiel hinzu, auf das ich antworte:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

14
Ich beantwortete dies (präziser) als Antwort auf die erste Version der Frage und sie erhielt 20 positive Stimmen, bevor sie von Bill the Lizard nach einigen Kommentaren und Bearbeitungen durch Darkness Races in Orbit gelöscht wurde.
Prost und hth. - Alf

1
@ Cheersandhth-Alf: Ich kann nicht glauben, dass es mod-gelöscht wurde. Das ist einfach schade. (+1)
Das paramagnetische Croissant

2
Mein +1 zu deinem Mut! (3 mal ist wichtig: D).
Haccks

Es ist absolut wichtig, dass Programmieranfänger etwas über Dinge wie die Auftragsausführung mit mehreren booleschen "ands" lernen, wie sich das in verschiedenen Sprachen unterscheidet usw. Und diese Antwort ist ein guter Hinweis darauf. Aber es ist nur ein "Nichtstarter" in der normalen kommerziellen Programmierung. Es ist nicht einfach, es ist nicht wartbar, es kann nicht geändert werden. Sicher, wenn Sie nur einen kurzen "Cowboy-Code" für sich selbst schreiben, tun Sie dies. Aber es hat einfach eine Art "keine Verbindung zum alltäglichen Software-Engineering, wie es heute praktiziert wird". Übrigens entschuldigen Sie die lächerliche "Edit-Verwirrung", die Sie hier ertragen mussten, @cheers :)
Fattie

1
@ JoeBlow: Ich bin mit Alf dabei - ich finde die &&Liste viel klarer. Normalerweise unterbreche ich die Bedingungen, um Zeilen zu trennen und // explanationjeder Zeile ein nachfolgendes hinzuzufügen ... Am Ende ist es massiv weniger Code zu betrachten, und wenn Sie erst einmal verstanden haben, wie es &&funktioniert, gibt es keine laufenden mentalen Anstrengungen mehr. Mein Eindruck ist, dass die meisten professionellen C ++ - Programmierer damit vertraut sind, aber wie Sie in verschiedenen Branchen / Projekten sagen, unterscheiden sich Fokus und Erfahrung.
Tony Delroy

35

Es gibt tatsächlich eine Möglichkeit, Aktionen in C ++ zu verschieben: Verwenden des Destruktors eines Objekts.

Angenommen, Sie haben Zugriff auf C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

Und dann mit diesem Dienstprogramm:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

67
Dies ist für mich nur eine völlige Verschleierung. Ich verstehe wirklich nicht, warum so viele C ++ - Programmierer ein Problem gerne lösen, indem sie so viele Sprachfunktionen wie möglich darauf werfen, bis jeder vergessen hat, welches Problem Sie tatsächlich gelöst haben: Sie kümmern sich nicht mehr darum, weil sie es sind Jetzt so interessiert daran, all diese exotischen Sprachfunktionen zu nutzen. Und von da an können Sie sich tagelang und wochenlang damit beschäftigen, Metacode zu schreiben, dann den Metacode zu pflegen und dann Metacode für den Umgang mit dem Metacode zu schreiben.
Lundin

24
@Lundin: Nun, ich verstehe nicht, wie man mit sprödem Code zufrieden sein kann, der kaputt geht, sobald ein frühes Fortsetzen / Brechen / Zurückgeben eingeführt oder eine Ausnahme ausgelöst wird. Andererseits ist diese Lösung angesichts zukünftiger Wartungsarbeiten robust und beruht nur auf der Tatsache, dass Destruktoren während des Abwickelns ausgeführt werden, was eines der wichtigsten Merkmale von C ++ ist. Dies als exotisch zu qualifizieren, während es das zugrunde liegende Prinzip ist, das alle Standardcontainer antreibt, ist gelinde gesagt amüsant.
Matthieu M.

7
@Lundin: Der Vorteil von Matthieus Code ist, dass er auch executeThisFunctionInAnyCase();dann ausgeführt wird, wenn foo();eine Ausnahme ausgelöst wird . Wenn Sie ausnahmesicheren Code schreiben, empfiehlt es sich, alle diese Bereinigungsfunktionen in einem Destruktor abzulegen.
Brian

6
@Brian Dann wirf keine Ausnahme ein foo(). Und wenn Sie es tun, fangen Sie es. Problem gelöst. Beheben Sie Fehler, indem Sie sie beheben, nicht indem Sie Workarounds schreiben.
Lundin

18
@Lundin: Die DeferKlasse ist ein wiederverwendbarer kleiner Code, mit dem Sie auf ausnahmsichere Weise jede Bereinigung am Ende des Blocks durchführen können. Es ist allgemein bekannt als Scope Guard . Ja, jede Verwendung eines Scope Guard kann auf andere, manuellere Weise ausgedrückt werden, genau wie jede forSchleife als Block und whileSchleife ausgedrückt werden kann, die wiederum mit ifund ausgedrückt werden können und gotodie auf Wunsch in Assemblersprache ausgedrückt werden können. oder für diejenigen, die wahre Meister sind, indem sie die Bits im Gedächtnis durch kosmische Strahlen verändern, die durch den Schmetterlingseffekt spezieller kurzer Grunzer und Gesänge gerichtet sind. aber warum das tun?
Prost und hth. - Alf

34

Es gibt eine nette Technik, die keine zusätzliche Wrapper-Funktion mit den return-Anweisungen benötigt (die von Itjax vorgeschriebene Methode). Es wird eine do- while(0)Pseudo-Schleife verwendet. Das while (0)stellt sicher, dass es sich tatsächlich nicht um eine Schleife handelt, sondern nur einmal ausgeführt wird. Die Schleifensyntax ermöglicht jedoch die Verwendung der break-Anweisung.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

11
Die Verwendung einer Funktion mit mehreren Rückgaben ist meiner Meinung nach ähnlich, aber besser lesbar.
Lundin

4
Ja, definitiv ist es besser lesbar ... Unter Effizienzgesichtspunkten kann jedoch der zusätzliche Funktionsaufruf (Parameterübergabe und -rückgabe) mit dem Konstrukt do {} while (0) vermieden werden.
Debasis

5
Sie können die Funktion weiterhin ausführen inline. Auf jeden Fall ist dies eine gute Technik, da sie bei mehr als diesem Problem hilft.
Ruslan

2
@Lundin Sie müssen die Codelokalität berücksichtigen. Das Verteilen von Code auf zu viele Orte hat ebenfalls Probleme.
API-Beast

3
Nach meiner Erfahrung ist dies eine sehr ungewöhnliche Redewendung. Ich würde eine Weile brauchen, um herauszufinden, was zum Teufel los war, und das ist ein schlechtes Zeichen, wenn ich Code überprüfe. Da es keine Vorteile gegenüber den anderen, allgemeineren und daher besser lesbaren Ansätzen zu haben scheint, konnte ich mich nicht abmelden.
Cody Gray

19

Sie können dies auch tun:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

Auf diese Weise haben Sie eine minimale lineare Wachstumsgröße von +1 Zeile pro Anruf und können problemlos gewartet werden.


EDIT : (Danke @Unda) Kein großer Fan, weil du die Sichtbarkeit verlierst IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
Es ging um die versehentlichen Funktionsaufrufe in push_back (), aber Sie haben es trotzdem behoben :)
Quentin

4
Ich würde gerne wissen, warum dies abgelehnt wurde. Angenommen, die Ausführungsschritte sind tatsächlich symmetrisch, wie es scheint, dann ist es sauber und wartbar.
ClickRick

1
Obwohl dies wohl sauberer aussehen könnte. Für Menschen ist es möglicherweise schwieriger zu verstehen, und für den Compiler ist es definitiv schwieriger zu verstehen!
Roy T.

1
Es ist oft eine gute Idee, Ihre Funktionen eher als Daten zu behandeln. Sobald Sie dies getan haben, werden Sie auch spätere Refactorings bemerken. Noch besser ist, wenn jedes Teil, das Sie bearbeiten, eine Objektreferenz ist, nicht nur eine Funktionsreferenz - dies gibt Ihnen noch mehr Möglichkeiten, Ihren Code auf der ganzen Linie zu verbessern.
Bill K

3
Für einen trivialen Fall etwas überarbeitet, aber diese Technik hat definitiv eine nette Funktion, die andere nicht haben: Sie können die Ausführungsreihenfolge und die Anzahl der zur Laufzeit aufgerufenen Funktionen ändern, und das ist schön :)
Xocoatzin

18

Würde das funktionieren? Ich denke, das entspricht Ihrem Code.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
Normalerweise rufe ich die Variable auf, okwenn ich dieselbe Variable wie diese verwende.
Macke

1
Es würde mich sehr interessieren, warum die Abstimmungen. Was läuft hier falsch?
ABCplus

1
Vergleichen Sie Ihre Antwort mit dem Kurzschlussansatz für die zyklomatische Komplexität.
AlphaGoku

14

Angenommen, der gewünschte Code ist so, wie ich ihn derzeit sehe:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

Ich würde sagen, dass der richtige Ansatz, da er am einfachsten zu lesen und am einfachsten zu pflegen ist, weniger Einrückungsstufen aufweist, was (derzeit) der erklärte Zweck der Frage ist.

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Dies vermeidet jede Notwendigkeit gotos, Ausnahmen, Dummy - whileSchleifen oder andere schwierigen Konstrukte und einfach wird auf mit der einfachen Arbeit an der Hand.


Bei Verwendung von Schleifen ist es normalerweise akzeptabel, die Schleife zu verwenden returnund aus ihr breakherauszuspringen, ohne zusätzliche "Flag" -Variablen einführen zu müssen. In diesem Fall wäre die Verwendung eines goto ähnlich harmlos - denken Sie daran, dass Sie zusätzliche goto-Komplexität gegen extra veränderbare variable Komplexität eintauschen.
Hugomg

2
@hugomg Die Variablen waren in der ursprünglichen Frage. Hier gibt es keine zusätzliche Komplexität. Es wurden Annahmen zu der Frage getroffen (z. B. dass die Variablen im redigierten Code benötigt werden), sodass sie beibehalten wurden. Wenn sie nicht benötigt werden, kann der Code vereinfacht werden, aber angesichts der Unvollständigkeit der Frage gibt es keine andere Annahme, die gültig gemacht werden kann.
ClickRick

Sehr nützlicher Ansatz, insb. Für die Verwendung durch Selbsternannte newbiebietet es eine sauberere Lösung ohne Nachteile. Ich stelle fest, dass es auch nicht darauf ankommt, stepsdass die gleiche Signatur vorliegt oder sogar Funktionen statt Blöcke sind. Ich kann sehen, dass dies als Refactor für den ersten Durchgang verwendet wird, selbst wenn ein differenzierterer Ansatz gültig ist.
Keith

12

Könnte die break-Anweisung auf irgendeine Weise verwendet werden?

Vielleicht nicht die beste Lösung, aber Sie können Ihre Anweisungen in eine do .. while (0)Schleife setzen und breakstattdessen Anweisungen verwenden return.


2
Es war nicht ich, der es abgelehnt hat, aber dies wäre ein Missbrauch eines Schleifenkonstrukts für etwas, bei dem der Effekt das ist, was derzeit gewünscht wird, aber unweigerlich zu Schmerzen führen würde. Wahrscheinlich für den nächsten Entwickler, der es in 2 Jahren warten muss, nachdem Sie zu einem anderen Projekt übergegangen sind.
ClickRick

3
@ClickRick, das do .. while (0)für Makrodefinitionen verwendet wird, missbraucht ebenfalls Schleifen, wird jedoch als OK betrachtet.
Ouah

1
Vielleicht, aber es gibt sauberere Wege, um dies zu erreichen.
ClickRick

4
@ClickRick Der einzige Zweck meiner Antwort ist die Antwort. Könnte die Anweisung break in irgendeiner Weise verwendet werden und die Antwort lautet ja. Die ersten Wörter in meiner Antwort deuten darauf hin, dass dies möglicherweise nicht die zu verwendende Lösung ist.
Ouah

2
Diese Antwort sollte nur ein Kommentar sein
msmucker0527

12

Sie können alle ifBedingungen, die nach Ihren Wünschen formatiert sind, in eine eigene executeThisFunctionInAnyCase()Funktion einfügen und die Funktion bei der Rückgabe ausführen .

Aus dem Basisbeispiel im OP können die Bedingungstests und die Ausführung als solche abgespalten werden.

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

Und dann als solche bezeichnet;

InitialSteps();
executeThisFunctionInAnyCase();

Wenn C ++ 11-Lambdas verfügbar sind (es gab kein C ++ 11-Tag im OP, aber sie sind möglicherweise noch eine Option), können wir auf die separate Funktion verzichten und diese in ein Lambda einpacken.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

Wenn Sie Loops nicht mögen gotound nicht mögen do { } while (0);und C ++ verwenden möchten, können Sie auch ein temporäres Lambda verwenden, um den gleichen Effekt zu erzielen.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

1
if Sie mögen && es nicht, Sie mögen es nicht, {} zu tun, während (0) && Sie C ++ mögen ... Entschuldigung, ich konnte nicht widerstehen, aber diese letzte Bedingung schlägt fehl, weil die Frage sowohl mit c als auch mit c ++
ClickRick

@ClickRick es ist immer schwierig, alle zufrieden zu stellen. In meiner Meinung nach gibt es Sie so etwas wie C / C ++ nicht in der Regel Code in einer von denen , und die Nutzung des anderen wird nicht gerne gesehen.
Alex

9

Die Ketten von IF / ELSE in Ihrem Code sind nicht das Sprachproblem, sondern das Design Ihres Programms. Wenn Sie in der Lage sind, Ihr Programm neu zu faktorisieren oder neu zu schreiben, würde ich vorschlagen, dass Sie in Design Patterns ( http://sourcemaking.com/design_patterns ) nach einer besseren Lösung suchen.

Wenn Sie viele IFs und andere in Ihrem Code sehen, ist dies normalerweise eine Gelegenheit, das Strategie-Design-Muster ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) oder eine Kombination zu implementieren von anderen Mustern.

Ich bin mir sicher, dass es Alternativen gibt, um eine lange Liste von if / else zu schreiben, aber ich bezweifle, dass sie irgendetwas ändern werden, außer dass die Kette für Sie hübsch aussieht (jedoch liegt die Schönheit im Auge des Betrachters immer noch für Code auch:-) ) . Sie sollten sich Sorgen machen über Dinge wie (in 6 Monaten, wenn ich einen neuen Zustand habe und mich an nichts über diesen Code erinnere, kann ich ihn einfach hinzufügen? Oder was ist, wenn sich die Kette ändert, wie schnell und fehlerfrei werde ich es umsetzen)


9

Du machst das einfach ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 mal von 100, das ist der einzige Weg, es zu tun.

Versuchen Sie niemals, etwas "Kniffliges" im Computercode zu tun.


Ich bin mir übrigens ziemlich sicher, dass das Folgende die eigentliche Lösung ist, die Sie sich vorgestellt haben ...

Die continue- Anweisung ist für die algorithmische Programmierung von entscheidender Bedeutung. (Ebenso wie die goto-Anweisung bei der algorithmischen Programmierung von entscheidender Bedeutung ist.)

In vielen Programmiersprachen können Sie dies tun:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");

    int x = 69;

    {

    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }

    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }

    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }

    }

    NSLog(@"code g");
    }

(Beachten Sie zunächst: Nackte Blöcke wie dieses Beispiel sind ein kritischer und wichtiger Bestandteil beim Schreiben von schönem Code, insbesondere wenn Sie sich mit "algorithmischer" Programmierung beschäftigen.)

Auch das ist genau das, was Sie in Ihrem Kopf hatten, nicht wahr? Und das ist die schöne Art, es zu schreiben, also hast du gute Instinkte.

Tragischerweise gibt es in der aktuellen Version von Objective-C (abgesehen davon - ich weiß leider nichts über Swift) eine risikoreiche Funktion, bei der überprüft wird, ob der umschließende Block eine Schleife ist.

Geben Sie hier die Bildbeschreibung ein

Hier erfahren Sie, wie Sie das umgehen können ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");

    int x = 69;

    do{

    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }

    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }

    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }

    }while(false);

    NSLog(@"code g");
    }

Also vergiss das nicht ..

do {} while (false);

bedeutet nur "mache diesen Block einmal".

Das heißt, es gibt absolut keinen Unterschied zwischen Schreiben do{}while(false);und einfachem Schreiben {}.

Dies funktioniert jetzt perfekt wie Sie wollten ... hier ist die Ausgabe ...

Geben Sie hier die Bildbeschreibung ein

Es ist also möglich, dass Sie den Algorithmus so in Ihrem Kopf sehen. Sie sollten immer versuchen zu schreiben, was in Ihrem Kopf ist. (Besonders wenn du nicht nüchtern bist, denn dann kommt das Hübsche raus! :))

In "algorithmischen" Projekten, in denen dies häufig vorkommt, haben wir in Ziel-c immer ein Makro wie ...

#define RUNONCE while(false)

... also dann kannst du das machen ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;

    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE

    NSLog(@"code g");
    }

Es gibt zwei Punkte:

a, obwohl es dumm ist, dass objectiv-c die Art des Blocks überprüft, in dem sich eine continue-Anweisung befindet, ist es beunruhigend, "das zu bekämpfen". Es ist also eine schwierige Entscheidung.

b, gibt es die Frage, ob Sie im Beispiel diesen Block einrücken sollten? Ich verliere den Schlaf wegen solcher Fragen, daher kann ich nicht raten.

Ich hoffe es hilft.


Sie haben die zweitbeste Antwort verpasst .
Palec

Hast du das Kopfgeld eingesetzt, um mehr Wiederholungen zu bekommen? :)
TMS

Anstatt all diese Kommentare in das Feld einzufügen if, können Sie auch aussagekräftigere Funktionsnamen verwenden und die Kommentare in die Funktionen einfügen.
Thomas Ahle

Yuck. Ich werde die 1-Zeilen-Lösung, die sich mit der Kurzschlussbewertung (die seit über 20 Jahren in Sprachen verfügbar ist und bekannt ist) präzise liest, jeden Tag über dieses Chaos hinwegsetzen. Ich denke, wir können uns beide einig sein, dass wir glücklich sind, nicht miteinander zu arbeiten.
M2tM

8

Lassen Sie Ihre Ausführungsfunktionen eine Ausnahme auslösen, wenn sie fehlschlagen, anstatt false zurückzugeben. Dann könnte Ihr Anrufcode folgendermaßen aussehen:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Natürlich gehe ich davon aus, dass in Ihrem ursprünglichen Beispiel der Ausführungsschritt nur dann false zurückgeben würde, wenn innerhalb des Schritts ein Fehler auftritt.


3
Die Verwendung einer Ausnahme zur Steuerung des Flusses wird oft als schlechte Praxis und stinkender Code angesehen
user902383

8

Viele gute Antworten bereits, aber die meisten scheinen einen Teil (zugegebenermaßen sehr wenig) der Flexibilität zu beeinträchtigen. Ein gängiger Ansatz, der diesen Kompromiss nicht erfordert, ist das Hinzufügen einer Status- / Keep-Going- Variablen. Der Preis ist natürlich ein zusätzlicher Wert, den Sie im Auge behalten sollten:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

Warum ok &= conditionX;und nicht einfach ok = conditionX;?
ABCplus

@ user3253359 In vielen Fällen können Sie das tun. Dies ist eine konzeptionelle Demo. im
Arbeitscode

+1 Eine der wenigen sauberen und wartbaren Antworten, die in c funktioniert , wie in der Frage angegeben.
ClickRick

6

Wenn Sie in C ++ (die Frage ist sowohl mit C als auch mit C ++ gekennzeichnet) die Funktionen nicht ändern können, um Ausnahmen zu verwenden, können Sie den Ausnahmemechanismus trotzdem verwenden, wenn Sie eine kleine Hilfsfunktion wie schreiben

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Dann könnte Ihr Code wie folgt lauten:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Wenn Sie sich für ausgefallene Syntax interessieren, können Sie sie stattdessen über eine explizite Besetzung zum Laufen bringen:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Dann können Sie Ihren Code als schreiben

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Das Refactoring von Wertprüfungen in Ausnahmen ist nicht unbedingt ein guter Weg, es gibt erhebliche Overhead-Abwicklungsausnahmen.
John Wu

4
-1 Die Verwendung von Ausnahmen für den normalen Ablauf in C ++ ist eine schlechte Programmierpraxis. In C ++ sollten Ausnahmen für außergewöhnliche Umstände reserviert werden.
Jack Aidley

1
Aus dem Fragetext (Hervorhebung von mir): "Funktionen executeStepX sollte genau dann ausgeführt werden, wenn der vorherige erfolgreich ist. " Mit anderen Worten, der Rückgabewert wird verwendet, um einen Fehler anzuzeigen. Das heißt, diese Fehlerbehandlung (und man würde hoffen , dass Ausfälle sind außergewöhnlich). Die Fehlerbehandlung ist genau das , wofür Ausnahmen erfunden wurden.
Celtschk

1
Nee. Zunächst wurden Ausnahmen erstellt, um die Fehlerausbreitung und nicht die Fehlerbehandlung zu ermöglichen . zweitens "Funktionen executeStepX sollte genau dann ausgeführt werden, wenn die vorherige erfolgreich ist." bedeutet nicht, dass der von der vorherigen Funktion zurückgegebene boolesche Wert false einen offensichtlich außergewöhnlichen / fehlerhaften Fall anzeigt. Ihre Aussage ist also nicht sequitur . Fehlerbehandlung und Flussbereinigung können auf viele verschiedene Arten implementiert werden. Ausnahmen sind ein Werkzeug, das die Fehlerausbreitung und die Fehlerbehandlung an Ort und Stelle ermöglicht , und zeichnen sich dadurch aus.

6

Wenn Ihr Code so einfach wie Ihr Beispiel ist und Ihre Sprache Kurzschlussauswertungen unterstützt, können Sie Folgendes versuchen:

StepA() && StepB() && StepC() && StepD();
DoAlways();

Wenn Sie Argumente an Ihre Funktionen übergeben und andere Ergebnisse zurückerhalten, sodass Ihr Code nicht auf die vorherige Weise geschrieben werden kann, sind viele der anderen Antworten besser für das Problem geeignet.


Tatsächlich habe ich meine Frage bearbeitet, um das Thema besser zu erklären, aber es wurde abgelehnt, die meisten Antworten nicht ungültig zu machen. : \
ABCplus

Ich bin ein neuer Benutzer in SO und ein neuer Programmierer. 2 Fragen: Besteht das Risiko, dass eine andere Frage wie die von Ihnen angegebene aufgrund dieser Frage als dupliziert markiert wird? Ein weiterer Punkt ist: Wie könnte ein Neuling SO-Benutzer / Programmierer die beste Antwort zwischen allen dort wählen (fast gut, nehme ich an ..)?
ABCplus

6

Für C ++ 11 und höher könnte ein guter Ansatz darin bestehen, ein Scope-Exit- System zu implementieren, das dem Scope-Exit- Mechanismus (Exit- Mechanismus ) von D ähnelt .

Eine Möglichkeit zur Implementierung ist die Verwendung von C ++ 11-Lambdas und einigen Hilfsmakros:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

Auf diese Weise können Sie frühzeitig von der Funktion zurückkehren und sicherstellen, dass der von Ihnen definierte Bereinigungscode immer beim Beenden des Bereichs ausgeführt wird:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Die Makros sind wirklich nur Dekoration. MakeScopeExit()kann direkt verwendet werden.


Es sind keine Makros erforderlich, damit dies funktioniert. Und [=]ist normalerweise falsch für ein Lambda mit Zielfernrohr.
Yakk - Adam Nevraumont

Ja, die Makros dienen nur zur Dekoration und können weggeworfen werden. Aber würden Sie nicht sagen, dass die Erfassung nach Wert der sicherste "generische" Ansatz ist?
Glampert

1
Nein: Wenn Ihr Lambda nicht über den aktuellen Umfang hinaus bestehen bleibt, in dem das Lambda erstellt wird, verwenden Sie [&]: Es ist sicher und minimal überraschend. Erfassung nur nach Wert, wenn das Lambda (oder die Kopien) länger als der Umfang zum Zeitpunkt der Erklärung überleben könnten ...
Yakk - Adam Nevraumont

Ja, das macht Sinn. Ich werde es ändern. Vielen Dank!
Glampert

6

Warum gibt niemand die einfachste Lösung? : D.

Wenn alle Ihre Funktionen dieselbe Signatur haben, können Sie dies folgendermaßen tun (für die Sprache C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Für eine saubere C ++ - Lösung sollten Sie eine Schnittstellenklasse erstellen, die eine Ausführungsmethode enthält und Ihre Schritte in Objekte einschließt.
Dann sieht die obige Lösung folgendermaßen aus:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

5

Angenommen, Sie benötigen keine einzelnen Bedingungsvariablen. Wenn Sie die Tests invertieren und das else-falthrough als "ok" -Pfad verwenden, erhalten Sie eine vertikalere Menge von if / else-Anweisungen:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

Das Weglassen der fehlgeschlagenen Variablen macht den Code IMO etwas zu dunkel.

Das Deklarieren der Variablen im Inneren ist in Ordnung, keine Sorge um = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Das ist dunkel, aber kompakt:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

Dies sieht aus wie eine Zustandsmaschine, was praktisch ist, da Sie sie einfach mit einem Zustandsmuster implementieren können .

In Java würde es ungefähr so ​​aussehen:

interface StepState{
public StepState performStep();
}

Eine Implementierung würde wie folgt funktionieren:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

Und so weiter. Dann können Sie die große if-Bedingung durch Folgendes ersetzen:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

Wie Rommik erwähnte, könnten Sie hierfür ein Entwurfsmuster anwenden, aber ich würde das Decorator-Muster anstelle der Strategie verwenden, da Sie Anrufe verketten möchten. Wenn der Code einfach ist, würde ich eine der gut strukturierten Antworten verwenden, um eine Verschachtelung zu verhindern. Wenn es jedoch komplex ist oder eine dynamische Verkettung erfordert, ist das Decorator-Muster eine gute Wahl. Hier ist ein yUML-Klassendiagramm :

yUML Klassendiagramm

Hier ist ein Beispiel für ein LinqPad C # -Programm:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

Das beste Buch zum Lesen von Designmustern, IMHO, ist Head First Design Patterns .


Was ist der Vorteil davon gegenüber so etwas wie Jefffreys Antwort?
Dason

Wenn sich die Anforderungen ändern, ist dieser Ansatz ohne viel Domänenwissen einfacher zu verwalten. Besonders wenn man bedenkt, wie tief und lang einige Abschnitte verschachtelter Ifs werden können. Es kann alles sehr zerbrechlich werden und daher ein hohes Risiko bestehen, damit zu arbeiten. Verstehen Sie mich nicht falsch. Einige Optimierungsszenarien können dazu führen, dass Sie dies herausreißen und zu ifs zurückkehren, aber in 99% der Fälle ist dies in Ordnung. Aber der Punkt ist, wenn Sie dieses Niveau erreichen, ist Ihnen die Wartbarkeit egal, Sie benötigen die Leistung.
John Nicholas

4

Mehrere Antworten deuteten auf ein Muster hin, das ich oft gesehen und verwendet habe, insbesondere bei der Netzwerkprogrammierung. In Netzwerkstapeln gibt es häufig eine lange Folge von Anforderungen, von denen jede fehlschlagen kann und den Prozess stoppt.

Das übliche Muster war zu verwenden do { } while (false);

Ich habe ein Makro verwendet while(false), um es zu erstellen. do { } once;Das übliche Muster war:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

Dieses Muster war relativ einfach zu lesen und ermöglichte die Verwendung von Objekten, die ordnungsgemäß zerstört und mehrere Rückgaben vermieden wurden, was das Schritt- und Debugging etwas erleichterte.


4

Um die C ++ 11-Antwort von Mathieu zu verbessern und die Laufzeitkosten zu vermeiden, die durch die Verwendung von entstehen std::function, würde ich empfehlen, Folgendes zu verwenden

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Diese einfache Vorlagenklasse akzeptiert jeden Funktor, der ohne Parameter aufgerufen werden kann, und zwar ohne dynamische Speicherzuweisungen und entspricht daher besser dem Abstraktionsziel von C ++ ohne unnötigen Overhead. Die zusätzliche Funktionsvorlage soll die Verwendung durch Ableiten von Vorlagenparametern vereinfachen (was für Klassenvorlagenparameter nicht verfügbar ist).

Anwendungsbeispiel:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Genau wie Mathieus Antwort ist diese Lösung völlig ausnahmesicher und executeThisFunctionInAnyCasewird in jedem Fall aufgerufen. Sollte sich executeThisFunctionInAnyCaseselbst werfen, werden Destruktoren implizit markiert noexceptund daher wird ein Aufruf von std::terminateausgegeben, anstatt zu bewirken, dass beim Abwickeln des Stapels eine Ausnahme ausgelöst wird.


+1 Ich habe nach dieser Antwort gesucht, damit ich sie nicht posten muss. Sie sollten functorim deferredKonstruktor perfekt vorwärts gehen , ohne a erzwingen zu müssen move.
Yakk - Adam Nevraumont

@ Yakk änderte den Konstruktor in einen Weiterleitungskonstruktor
Joe

3

Anscheinend möchten Sie Ihren gesamten Anruf von einem einzigen Block aus erledigen. Wie andere vorgeschlagen haben, sollten Sie entweder eine whileSchleife verwenden und breakeine neue Funktion verwenden, die Sie verlassen können return(möglicherweise sauberer).

Ich persönlich verbanne goto, auch für den Funktionsausgang. Sie sind beim Debuggen schwerer zu erkennen.

Eine elegante Alternative, die für Ihren Workflow funktionieren sollte, besteht darin, ein Funktionsarray zu erstellen und dieses zu iterieren.

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();

Glücklicherweise erkennt der Debugger sie für Sie. Wenn Sie debuggen und nicht einmal durch Code gehen, machen Sie es falsch.
Cody Gray

Ich verstehe nicht, was du meinst, und auch nicht, warum ich kein Einzelschritt verwenden konnte.
AxFab

3

Da Sie zwischen den Ausführungen auch einen [... Codeblock ...] haben , haben Sie vermutlich Speicherzuweisung oder Objektinitialisierungen. Auf diese Weise müssen Sie sich darum kümmern, alles zu bereinigen, was Sie bereits beim Beenden initialisiert haben, und es auch bereinigen, wenn Sie auf ein Problem stoßen und eine der Funktionen false zurückgibt.

In diesem Fall war das Beste, was ich in meiner Erfahrung (als ich mit CryptoAPI arbeitete) hatte, das Erstellen kleiner Klassen. Im Konstruktor initialisieren Sie Ihre Daten, im Destruktor initialisieren Sie sie nicht. Jede nächste Funktionsklasse muss der vorherigen Funktionsklasse untergeordnet sein. Wenn etwas schief gelaufen ist - Ausnahme auslösen.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

Und dann müssen Sie in Ihrem Code nur noch anrufen:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Ich denke, es ist die beste Lösung, wenn jeder Aufruf von ConditionX etwas initialisiert, Speicher usw. zuweist. Am besten stellen Sie sicher, dass alles bereinigt wird.


3

Ein interessanter Weg ist, mit Ausnahmen zu arbeiten.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Wenn Sie solchen Code schreiben, gehen Sie irgendwie in die falsche Richtung. Ich werde es nicht als "das Problem" ansehen, einen solchen Code zu haben, sondern eine so chaotische "Architektur".

Tipp: Besprechen Sie diese Fälle mit einem erfahrenen Entwickler, dem Sie vertrauen ;-)


Ich denke, diese Idee kann nicht jede If-Kette ersetzen. In vielen Fällen ist dies jedoch ein sehr guter Ansatz!
WoIIe

3

Eine andere do - whileAnsatzschleife, obwohl sie bereits erwähnt wurde, gab es kein Beispiel dafür, das zeigen würde, wie sie aussieht:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Nun, es gibt bereits eine Antwort mit whileSchleife, aber die do - whileSchleife prüft nicht redundant (am Anfang) auf wahr, sondern am Ende xD (dies kann jedoch übersprungen werden)).


Hey Zaffy - diese Antwort enthält eine massive Erklärung des do {} while (false) -Ansatzes. stackoverflow.com/a/24588605/294884 Zwei andere Antworten erwähnten es ebenfalls.
Fattie

Es ist eine faszinierende Frage, ob es in dieser Situation eleganter ist, CONTINUE oder BREAK zu verwenden!
Fattie

Hey @ JoeBlow Ich habe alle Antworten gesehen ... wollte nur zeigen, anstatt darüber zu sprechen :)
Zaffy

Meine erste Antwort hier sagte ich "Niemand hat DIESES erwähnt ..." und sofort wies jemand freundlich darauf hin, dass es die 2. Top Antwort war :)
Fattie

@ JoeBlow Eh, du hast recht. Ich werde versuchen, das zu beheben. Ich fühle mich wie ... xD Trotzdem danke, nächstes Mal zahle ich etwas mehr
Aufmerksamkeit
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.