webentwicklung-frage-antwort-db.com.de

Invertieren Sie die "if" -Anweisung, um die Verschachtelung zu verringern

Wenn ich ReSharper für meinen Code ausgeführt habe, zum Beispiel:

    if (some condition)
    {
        Some code...            
    }

ReSharper gab mir die obige Warnung (Invert "if" -Anweisung, um die Verschachtelung zu reduzieren) und schlug die folgende Korrektur vor:

   if (!some condition) return;
   Some code...

Ich würde gerne verstehen, warum das besser ist. Ich dachte immer, dass die Verwendung von "return" in der Mitte einer Methode problematisch ist, ähnlich wie "goto".

250
Lea Cohen

Eine Rückkehr mitten in der Methode ist nicht unbedingt schlecht. Es ist möglicherweise besser, sofort zurückzukehren, wenn dadurch die Absicht des Codes klarer wird. Beispielsweise:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

In diesem Fall, wenn _isDead ist wahr, wir können die Methode sofort verlassen. Es könnte besser sein, es stattdessen so zu strukturieren:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

Ich habe diesen Code aus dem Refactoring-Katalog ausgewählt. Dieses spezielle Refactoring heißt: Verschachtelte Bedingungen durch Schutzklauseln ersetzen.

274
jop

Es ist nicht nur ästhetisch , sondern reduziert auch das maximale Verschachtelungsnivea innerhalb der Methode. Dies wird allgemein als Pluspunkt angesehen, da es das Verständnis von Methoden erleichtert (und zwar vielestatischAnalyseWerkzeuge ein Maß dafür als einen der Indikatoren für die Codequalität angeben).

Andererseits hat Ihre Methode dadurch auch mehrere Austrittspunkte, was eine andere Gruppe von Menschen für ein Nein-Nein hält.

Persönlich stimme ich ReSharper und der ersten Gruppe zu (in einer Sprache mit Ausnahmen finde ich es dumm, "mehrere Austrittspunkte" zu diskutieren; fast alles kann werfen, daher gibt es zahlreiche potenzielle Austrittspunkte in allen Methoden).

In Bezug auf die Leistung : Beide Versionen sollten sind gleichwertig (wenn nicht auf IL-Ebene, dann sicherlich nachdem der Jitter vorbei ist mit dem Code) in jeder Sprache. Theoretisch hängt dies vom Compiler ab, aber praktisch jeder weit verbreitete Compiler von heute ist in der Lage, viel weiter fortgeschrittene Fälle der Codeoptimierung zu behandeln.

326
Jon

Dies ist ein religiöses Argument, aber ich stimme ReSharper zu, dass Sie weniger Schachteln bevorzugen sollten. Ich glaube, dass dies die Nachteile einer Funktion mit mehreren Rückkehrpfaden überwiegt.

Der Hauptgrund für eine geringere Verschachtelung ist die Verbesserung von Lesbarkeit und Wartbarkeit des Codes. Denken Sie daran, dass viele andere Entwickler Ihren Code in Zukunft lesen müssen und Code mit weniger Einrückungen im Allgemeinen viel einfacher zu lesen ist.

Voraussetzungen sind ein gutes Beispiel dafür, wo es in Ordnung ist, zu Beginn der Funktion frühzeitig zurückzukehren. Warum sollte die Lesbarkeit des Restes der Funktion durch das Vorhandensein einer Voraussetzungsprüfung beeinträchtigt werden?

Was die negativen Aspekte der mehrfachen Rückkehr von einer Methode angeht: Debugger sind jetzt ziemlich leistungsfähig, und es ist sehr einfach, genau herauszufinden, wo und wann eine bestimmte Funktion zurückkehrt.

Wenn eine Funktion mehrere Rückgaben enthält, hat dies keine Auswirkungen auf den Job des Wartungsprogrammierers.

Schlechte Lesbarkeit des Codes.

101

Wie andere bereits erwähnt haben, sollte es keine Leistungseinbußen geben, aber es gibt andere Überlegungen. Abgesehen von diesen berechtigten Bedenken kann dies unter bestimmten Umständen auch zu Fallstricken führen. Angenommen, Sie haben es stattdessen mit einem double zu tun:

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

Vergleichen Sie das mit der scheinbar äquivalenten Inversion:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

Unter bestimmten Umständen kann es also sein, dass das, was als korrekt invertiertes if erscheint, nicht so ist.

68
Michael McGowan

Die Idee, nur am Ende einer Funktion zurückzukehren, kam aus der Zeit, als Sprachen Ausnahmen unterstützten. Es ermöglichte es Programmen, sich darauf zu verlassen, dass Bereinigungscode am Ende einer Methode eingefügt werden kann, und dann sicher zu sein, dass er aufgerufen wird und ein anderer Programmierer keine Rückgabe in der Methode verbirgt, die das Überspringen des Bereinigungscodes verursacht . Übersprungener Bereinigungscode kann zu einem Speicher- oder Ressourcenleck führen.

In einer Sprache, die Ausnahmen unterstützt, gibt es jedoch keine solchen Garantien. In einer Sprache, die Ausnahmen unterstützt, kann die Ausführung einer Anweisung oder eines Ausdrucks einen Steuerfluss verursachen, der das Beenden der Methode verursacht. Dies bedeutet, dass die Bereinigung mithilfe des Befehls finally oder mithilfe von Schlüsselwörtern erfolgen muss.

Wie auch immer, ich sage, ich denke, viele Leute zitieren die Richtlinie "Nur am Ende einer Methode zurückkehren", ohne zu verstehen, warum dies jemals eine gute Sache war, und dass das Reduzieren der Verschachtelung zur Verbesserung der Lesbarkeit wahrscheinlich ein besseres Ziel ist.

50
Scott Langham

Ich möchte hinzufügen, dass es einen Namen für die invertierten if's - Guard-Klausel gibt. Ich benutze es wann immer ich kann.

Ich hasse es, Code zu lesen, wo es zu Beginn zwei Code-Bildschirme gibt und sonst nichts. Invertieren Sie einfach if und kehren Sie zurück. Auf diese Weise wird niemand Zeit mit Scrollen verschwenden.

http://c2.com/cgi/wiki?GuardClause

27
Piotr Perak

Dies beeinträchtigt nicht nur die Ästhetik, sondern verhindert auch das Verschachteln von Code.

Dies kann tatsächlich eine Voraussetzung dafür sein, dass auch Ihre Daten gültig sind.

21
Rion Williams

Das ist natürlich subjektiv, aber ich denke, es verbessert sich in zwei Punkten stark:

  • Es ist jetzt sofort klar, dass Ihre Funktion nichts mehr zu tun hat, wenn condition gilt.
  • Es hält die Verschachtelungsebene niedrig. Schachteln schadet der Lesbarkeit mehr als man denkt.
18
Deestan

Mehrere Rückgabepunkte waren in C (und in geringerem Maße in C++) ein Problem, da Sie gezwungen waren, den Bereinigungscode vor jedem der Rückgabepunkte zu duplizieren. Bei der Garbage Collection wird try | finally construct und using blocks, es gibt wirklich keinen Grund, warum du dich vor ihnen fürchten solltest.

Letztendlich kommt es darauf an, was Sie und Ihre Kollegen besser lesen können.

14
Richard Poole

In Bezug auf die Leistung wird es keinen merklichen Unterschied zwischen den beiden Ansätzen geben.

Beim Codieren geht es jedoch um mehr als um Leistung. Klarheit und Wartbarkeit sind ebenfalls sehr wichtig. Und in Fällen wie diesen, in denen die Leistung nicht beeinträchtigt wird, ist dies das einzige, was zählt.

Es gibt konkurrierende Denkschulen, welche Herangehensweise vorzuziehen ist.

Eine Ansicht ist diejenige, die andere erwähnt haben: Der zweite Ansatz reduziert die Verschachtelungsebene, wodurch die Code-Klarheit verbessert wird. Dies ist natürlich in einem imperativen Stil: Wenn Sie nichts mehr zu tun haben, können Sie genauso gut früh zurückkehren.

Eine andere Ansicht aus der Perspektive eines funktionaleren Stils ist, dass eine Methode nur einen Austrittspunkt haben sollte. Alles in einer funktionalen Sprache ist ein Ausdruck. If-Anweisungen müssen also immer eine else-Klausel enthalten. Andernfalls hätte der if-Ausdruck nicht immer einen Wert. Im funktionalen Stil ist der erste Ansatz also natürlicher.

12
Jeffrey Sax

Schutzklauseln oder Vorbedingungen (wie Sie wahrscheinlich sehen können) prüfen, ob eine bestimmte Bedingung erfüllt ist, und unterbrechen dann den Programmfluss. Sie eignen sich hervorragend für Orte, an denen Sie nur an einem Ergebnis einer if -Anweisung interessiert sind. Also anstatt zu sagen:

if (something) {
    // a lot of indented code
}

Sie stornieren die Bedingung und brechen ab, wenn diese stornierte Bedingung erfüllt ist

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

return ist bei weitem nicht so schmutzig wie goto. Sie können einen Wert übergeben, um den Rest Ihres Codes anzuzeigen, den die Funktion nicht ausführen konnte.

Sie sehen die besten Beispiele dafür, wo dies unter verschachtelten Bedingungen angewendet werden kann:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

vs:

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

Es gibt nur wenige Leute, die behaupten, der erste sei sauberer, aber natürlich ist er völlig subjektiv. Einige Programmierer möchten gerne wissen, unter welchen Bedingungen etwas durch Einrücken funktioniert, während ich den Methodenfluss lieber linear halte.

Ich werde nicht für einen Moment vorschlagen, dass Precons Ihr Leben verändern oder Sie zur Ruhe bringen, aber Sie werden Ihren Code vielleicht ein bisschen einfacher zu lesen finden.

11
Oli

Hier werden mehrere gute Punkte gemacht, aber auch mehrere Rückgabepunkte kann unlesbar sein, wenn die Methode sehr langwierig ist. Wenn Sie jedoch mehrere Rückgabepunkte verwenden möchten, stellen Sie sicher, dass Ihre Methode kurz ist. Andernfalls kann der Lesbarkeitsbonus mehrerer Rückgabepunkte verloren gehen.

9
Jon Limjap

Die Leistung besteht aus zwei Teilen. Sie haben Leistung, wenn die Software in Produktion ist, möchten aber auch Leistung beim Entwickeln und Debuggen. Das Letzte, was ein Entwickler möchte, ist, auf etwas Triviales zu "warten". Am Ende führt das Kompilieren mit aktivierter Optimierung zu ähnlichem Code. Es ist also gut, diese kleinen Tricks zu kennen, die sich in beiden Szenarien auszahlen.

Der Fall in der Frage ist klar, ReSharper ist richtig. Anstatt if Anweisungen zu verschachteln und neuen Bereich im Code zu erstellen, legen Sie zu Beginn Ihrer Methode eine klare Regel fest. Dies erhöht die Lesbarkeit, erleichtert die Wartung und verringert die Anzahl der Regeln, die durchsucht werden müssen, um herauszufinden, wohin sie gehen möchten.

8
Ryan Ternier

Persönlich bevorzuge ich nur 1 Ausstiegspunkt. Es ist einfach zu bewerkstelligen, wenn Sie Ihre Methoden kurz und sachlich halten, und es liefert ein vorhersehbares Muster für die nächste Person, die an Ihrem Code arbeitet.

z.B.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

Dies ist auch sehr nützlich, wenn Sie nur die Werte bestimmter lokaler Variablen in einer Funktion überprüfen möchten, bevor sie beendet wird. Alles, was Sie tun müssen, ist, einen Haltepunkt auf der endgültigen Rückgabe zu platzieren, und Sie werden ihn garantiert treffen (es sei denn, eine Ausnahme wird geworfen).

7
ilitirit

Viele gute Gründe für wie der Code aussieht. Aber was ist mit Ergebnisse?

Werfen wir einen Blick auf einen C # -Code und seine IL-kompilierte Form:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

Dieses einfache Snippet kann kompiliert werden. Sie können die generierte EXE-Datei mit ildasm öffnen und das Ergebnis überprüfen. Ich werde nicht die ganze Assembler-Sache posten, aber ich werde die Ergebnisse beschreiben.

Der generierte IL-Code führt Folgendes aus:

  1. Wenn die erste Bedingung falsch ist, wird zu dem Code gesprungen, in dem sich die zweite befindet.
  2. Wenn es wahr ist, springt es zur letzten Anweisung. (Hinweis: Die letzte Anweisung ist eine Rückgabe).
  3. In der zweiten Bedingung geschieht dasselbe, nachdem das Ergebnis berechnet wurde. Vergleiche und: gehe zu Console.WriteLine wenn falsch oder zum Ende wenn dies wahr ist.
  4. Drucken Sie die Nachricht und kehren Sie zurück.

Es sieht also so aus, als würde der Code ans Ende springen. Was ist, wenn wir ein normales Verfahren mit verschachteltem Code ausführen?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

Die Ergebnisse sind in IL-Anweisungen ziemlich ähnlich. Der Unterschied besteht darin, dass es vorher Sprünge pro Bedingung gab: Wenn falsch, gehe zum nächsten Teil des Codes, wenn wahr, gehe zum Ende. Und jetzt fließt der IL-Code besser und hat 3 Sprünge (der Compiler hat dies ein bisschen optimiert): 1. Erster Sprung: Wenn die Länge 0 ist, springt der Code wieder zu einem Teil (dritter Sprung) bis zum Ende. 2. Zweite: In der Mitte der zweiten Bedingung, um eine Anweisung zu vermeiden. 3. Drittens: Wenn die zweite Bedingung falsch ist, springen Sie zum Ende.

Auf jeden Fall springt der Programmzähler immer.

5
graffic

Das ist einfach umstritten. Es gibt keine "Übereinstimmung unter Programmierern" in der Frage der vorzeitigen Rückkehr. Soweit ich weiß, ist es immer subjektiv.

Es ist möglich, ein Leistungsargument vorzubringen, da es besser ist, Bedingungen zu haben, die so geschrieben sind, dass sie am häufigsten zutreffen. es kann auch argumentiert werden, dass es klarer ist. Auf der anderen Seite werden verschachtelte Tests erstellt.

Ich glaube nicht, dass Sie eine schlüssige Antwort auf diese Frage bekommen.

4
unwind

Theoretisch könnte das Invertieren von if zu einer besseren Leistung führen, wenn die Trefferrate für die Verzweigungsvorhersage erhöht wird. In der Praxis denke ich, dass es sehr schwer ist, genau zu wissen, wie sich die Verzweigungsvorhersage verhält, insbesondere nach dem Kompilieren. Daher würde ich dies in meiner täglichen Entwicklung nur tun, wenn ich Assembly-Code schreibe.

Mehr zur Verzweigungsvorhersage hier .

4
jfg956

Es gibt dort bereits viele aufschlussreiche Antworten, aber dennoch möchte ich auf eine etwas andere Situation eingehen: Anstelle einer Voraussetzung, die in der Tat auf eine Funktion gesetzt werden sollte, sollte an eine schrittweise Initialisierung gedacht werden, bei der Sie Sie müssen nach jedem Schritt suchen, um erfolgreich zu sein, und dann mit dem nächsten fortfahren. In diesem Fall können Sie nicht alles oben überprüfen.

Ich fand meinen Code beim Schreiben einer ASIO Host-Anwendung mit Steinbergs ASIOSDK wirklich unleserlich, da ich dem Schachtelungsparadigma folgte. Es war acht Ebenen tief und ich kann dort keinen Konstruktionsfehler erkennen, wie oben von Andrew Bullock erwähnt. Natürlich hätte ich einen inneren Code in eine andere Funktion packen und dann die verbleibenden Ebenen dort verschachteln können, um die Lesbarkeit zu verbessern, aber dies scheint mir eher zufällig zu sein.

Indem ich die Verschachtelung durch Schutzklauseln ersetzte, entdeckte ich sogar ein Missverständnis in Bezug auf einen Teil des Bereinigungscodes, der viel früher innerhalb der Funktion als am Ende hätte auftreten sollen. Mit verschachtelten Zweigen hätte ich das nie gesehen, man könnte sogar sagen, dass sie zu meinem Missverständnis geführt haben.

Dies könnte also eine andere Situation sein, in der invertierte ifs zu einem klareren Code beitragen können.

3

Das Vermeiden mehrerer Austrittspunkte kann zu Leistungssteigerungen führen. Ich bin mir bei C # nicht sicher, aber in C++ hängt die Optimierung des benannten Rückgabewerts (Copy Elision, ISO C++ '03 12.8/15) davon ab, dass ein einzelner Exit-Punkt vorhanden ist. Diese Optimierung vermeidet das Kopieren Ihres Rückgabewerts (in Ihrem speziellen Beispiel ist das egal). Dies kann zu erheblichen Leistungssteigerungen in engen Schleifen führen, da Sie jedes Mal, wenn die Funktion aufgerufen wird, einen Konstruktor und einen Destruktor speichern.

Aber für 99% der Fälle ist das Speichern der zusätzlichen Konstruktor- und Destruktoraufrufe nicht den Verlust der Lesbarkeit wert, den verschachtelte if -Blöcke einführen (wie andere hervorgehoben haben).

3
shibumi

Meiner Meinung nach ist eine vorzeitige Rückgabe in Ordnung, wenn Sie nur void zurückgeben (oder einen nutzlosen Rückgabecode, den Sie nie überprüfen werden), und dies kann die Lesbarkeit verbessern, da Sie das Verschachteln vermeiden und gleichzeitig explizit darauf hinweisen, dass Ihre Funktion erledigt ist.

Wenn Sie tatsächlich einen returnValue zurückgeben, ist die Verschachtelung in der Regel der bessere Weg, da Sie Ihren returnValue nur an einer Stelle (am Ende) zurückgeben und Ihren Code in vielen Fällen besser warten können.

2
JohnIdol

Es ist eine Ansichtssache.

Normalerweise vermeide ich einzeilige ifs und kehre mitten in einer Methode zurück.

Sie möchten keine Zeilen, wie sie in Ihrer Methode überall vorkommen, aber es gibt etwas zu sagen, um eine Reihe von Annahmen oben in Ihrer Methode zu überprüfen und Ihre eigentliche Arbeit nur dann zu erledigen, wenn sie alle bestanden haben.

2
Colin Pickard

Ich bin mir nicht sicher, aber ich denke, dass R # versucht, weite Sprünge zu vermeiden. Wenn Sie IF-ELSE haben, geht der Compiler folgendermaßen vor:

Bedingung false -> Weitersprung zu false_condition_label

true_condition_label: Befehl1 ... Befehl_n

false_condition_label: Befehl1 ... Befehl_n

endeblock

Wenn die Bedingung wahr ist, gibt es keinen Sprung und keinen Rollout-L1-Cache, aber der Sprung zu false_condition_label kann sehr weit sein, und der Prozessor muss seinen eigenen Cache rollen. Das Synchronisieren des Cache ist teuer. R # -Versuche ersetzen weite Sprünge in kurze Sprünge und in diesem Fall ist die Wahrscheinlichkeit größer, dass sich alle Anweisungen bereits im Cache befinden.

Ich denke, es hängt davon ab, was Sie bevorzugen, wie erwähnt, es gibt keine allgemeine Vereinbarung afaik. Um Ärger zu vermeiden, können Sie diese Art der Warnung auf "Hinweis" reduzieren.

0
Bluenuance

Meine Idee ist, dass die Rückkehr "mitten in einer Funktion" nicht so "subjektiv" sein sollte. Der Grund ist ganz einfach, nimm diesen Code:

 Funktion do_something (data) {
 
 if (! is_valid_data (data)) 
 return false; 
 
 
 etwas tun, das eine Stunde dauert (Daten); 
 
 istance = new object_with_very_painful_constructor (Daten); ); 
 return; 
 
} 
 connect_to_database (); 
 get_some_other_data (); 
 return; 
 } 

Vielleicht ist die erste "Rückkehr" nicht SO intuitiv, aber das spart wirklich. Es gibt zu viele "Ideen" zu sauberen Codes, die einfach mehr Übung benötigen, um ihre "subjektiven" schlechten Ideen zu verlieren.

0

Diese Art der Codierung hat mehrere Vorteile, aber für mich ist der große Gewinn: Wenn Sie schnell zurückkehren können, können Sie die Geschwindigkeit Ihrer Anwendung verbessern. IE Ich weiß, dass ich aufgrund von Vorbedingung X mit einem Fehler schnell zurückkehren kann. Dies beseitigt zuerst die Fehlerfälle und reduziert die Komplexität Ihres Codes. In vielen Fällen liegt das an der CPU Pipeline kann jetzt sauberer sein, es kann Pipeline-Abstürze oder -Umschaltungen stoppen. Zweitens, wenn Sie sich in einer Schleife befinden, können Sie durch schnelles Unterbrechen oder Zurückkehren eine Menge CPU sparen Sie können Ihre CPU-Pipeline beschädigen und sogar ein Problem bei der Speichersuche verursachen, was bedeutet, dass die CPU von außerhalb des Cache geladen werden muss Implementieren Sie eine abstrakte Vorstellung von korrektem Code. Wenn das einzige Werkzeug, das Sie haben, ein Hammer ist, dann sieht alles aus wie ein Nagel.

0