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

272

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

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

ReSharper gab mir die obige Warnung ("if" -Anweisung umkehren, 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 habe immer gedacht, dass die Verwendung von "return" mitten in einer Methode problematisch ist, ähnlich wie "goto".

Lea Cohen
quelle
1
Ich glaube, dass die Ausnahmeprüfung und -rückgabe am Anfang in Ordnung ist, aber ich würde die Bedingung so ändern, dass Sie direkt nach der Ausnahme suchen und nicht nach etwas (dh wenn (eine Bedingung) zurückkehrt).
Bruceatk
36
Nein, es wird nichts für die Leistung tun.
Seth Carnegie
3
Ich wäre versucht, eine ArgumentException auszulösen, wenn meiner Methode stattdessen schlechte Daten übergeben würden.
Asawyer
1
@asawyer Ja, hier gibt es eine ganze Nebenbesprechung über Funktionen, die unsinnige Eingaben zu verzeihen - im Gegensatz zur Verwendung eines Assertionsfehlers. Das Schreiben von Solid Code öffnete mir die Augen dafür. In diesem Fall wäre das so etwas wie ASSERT( exampleParam > 0 ).
Greg Hendershott
4
Aussagen beziehen sich auf den internen Zustand, nicht auf Parameter. Sie würden zunächst die Parameter validieren, sicherstellen, dass Ihr interner Status korrekt ist, und dann die Operation ausführen. In einem Release-Build können Sie die Zusicherungen entweder weglassen oder einem Herunterfahren der Komponente zuordnen.
Simon Richter

Antworten:

296

Eine Rückkehr in der Mitte 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 _isDeadkönnen wir , wenn dies zutrifft, 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 ausgewählt Refactoring-Katalog ausgewählt . Dieses spezielle Refactoring heißt: Ersetzen Sie verschachtelte Bedingungen durch Schutzklauseln.

jop
quelle
13
Dies ist ein wirklich schönes Beispiel! Der überarbeitete Code liest sich eher wie eine case-Anweisung.
Anders als
16
Wahrscheinlich nur Geschmackssache: Ich schlage vor, das 2. und 3. "Wenn" in "Sonst Wenn" zu ändern, um die Lesbarkeit noch weiter zu verbessern. Wenn man die "return" -Anweisung übersieht, ist immer noch klar, dass der folgende Fall nur geprüft wird, wenn der vorherige fehlgeschlagen ist, dh dass die Reihenfolge der Prüfungen wichtig ist.
Foraidt
2
In einem Beispiel, das so einfach ist, stimme ich zu, dass der zweite Weg besser ist, aber nur, weil es so offensichtlich ist, was passiert.
Andrew Bullock
Wie nehmen wir nun diesen hübschen Code, den Jop geschrieben hat, und verhindern, dass Visual Studio ihn auflöst und die Renditen in separate Zeilen setzt? Es ärgert mich wirklich, dass es den Code auf diese Weise neu formatiert. Macht diesen sehr lesbaren Code hässlich.
Mark T
@Mark T In Visual Studio gibt es Einstellungen, die verhindern, dass dieser Code aufgelöst wird.
Aaron Smith
333

Es ist nicht nur ästhetisch , sondern reduziert auch das maximale Verschachtelungsniveau innerhalb der Methode. Dies wird im Allgemeinen als ein Plus angesehen , weil es Methoden macht leichter zu verstehen (und in der Tat viele statischen Analyse - Tools bieten ein Maß dafür als einer der Indikatoren für die Qualität des Codes).

Auf der anderen Seite hat Ihre Methode 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 albern, "mehrere Austrittspunkte" zu diskutieren; fast alles kann werfen, daher gibt es in allen Methoden zahlreiche potenzielle Austrittspunkte).

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

Jon
quelle
41
einzelne Ausstiegspunkte? Wer braucht das?
sq33G
3
@ sq33G: Diese Frage zu SESE (und die Antworten natürlich) sind fantastisch. Danke für den Link!
Jon
Ich höre immer wieder in Antworten, dass es einige Leute gibt, die sich für einzelne Ausstiegspunkte einsetzen, aber ich habe nie jemanden gesehen, der dies tatsächlich befürwortet, insbesondere in Sprachen wie C #.
Thomas Bonini
1
@AndreasBonini: Das Fehlen eines Beweises ist kein Beweis für das Fehlen. :-)
Jon
Ja natürlich finde ich es nur seltsam, dass jeder das Bedürfnis hat zu sagen, dass einige Leute einen anderen Ansatz mögen, wenn diese Leute nicht das Bedürfnis haben, es selbst zu sagen =)
Thomas Bonini
102

Dies ist ein religiöses Argument, aber ich stimme ReSharper zu, dass Sie weniger Verschachtelung bevorzugen sollten. Ich glaube, dass dies die Nachteile überwiegt, mehrere Rückwege von einer Funktion zu haben.

Der Hauptgrund für weniger Verschachtelung ist die Verbesserung der 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 Vorbedingungsprüfung beeinträchtigt werden?

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

Das Vorhandensein mehrerer Rückgaben in einer Funktion hat keine Auswirkungen auf den Job des Wartungsprogrammierers.

Schlechte Lesbarkeit des Codes wird.

LeopardSkinPillBoxHat
quelle
2
Mehrere Rückgaben in einer Funktion wirken sich auf den Wartungsprogrammierer aus. Beim Debuggen einer Funktion und beim Suchen nach dem unerwünschten Rückgabewert muss der Betreuer an allen Stellen, die zurückkehren können, Haltepunkte setzen.
EvilTeach
10
Ich würde einen Haltepunkt auf die öffnende Klammer setzen. Wenn Sie die Funktion durchlaufen, können Sie nicht nur sehen, welche Überprüfungen in der richtigen Reihenfolge ausgeführt werden, sondern es wird auch deutlich, auf welche Prüfung die Funktion für diesen Lauf gestoßen ist.
John Dunagan
3
Stimmen Sie hier mit John überein ... Ich sehe das Problem nicht darin, nur die gesamte Funktion zu durchlaufen.
Nagler
5
@Nailer Sehr spät, da stimme ich besonders zu, denn wenn die Funktion zu groß ist, um sie zu durchlaufen, sollte sie sowieso in mehrere Funktionen aufgeteilt werden!
Aidiakapi
1
Stimmen Sie auch mit John überein. Vielleicht ist es ein Gedanke der alten Schule, den Haltepunkt ganz unten zu setzen, aber wenn Sie neugierig sind, was eine Funktion zurückgibt, werden Sie diese Funktion durchlaufen, um zu sehen, warum sie zurückgibt, was sie sowieso zurückgibt. Wenn Sie nur sehen möchten, was zurückgegeben wird, setzen Sie es in die letzte Klammer.
user441521
70

Wie andere bereits erwähnt haben, sollte es keinen Leistungseinbruch 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 doublestattdessen mit einem 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 Umkehrung:

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

Unter bestimmten Umständen scheint es also ifnicht so zu sein, was als korrekt invertiert erscheint .

Michael McGowan
quelle
4
Es sollte auch beachtet werden, dass die Resharper-Schnellkorrektur exampleParam> 0 in exampleParam <0 invertiert, nicht exampleParam <= 0, was mich erwischt hat.
Nickd
1
Und aus diesem Grund sollte dieser Scheck im Voraus ausgestellt und auch zurückgegeben werden, da der Entwickler der Meinung ist, dass ein Nan zu einer Rettung führen sollte.
user441521
51

Die Idee, erst am Ende einer Funktion zurückzukehren, kam aus den Tagen, bevor Sprachen Ausnahmen unterstützten. Es ermöglichte 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 dazu führte, dass der Bereinigungscode übersprungen wurde . Übersprungener Bereinigungscode kann zu einem Speicher- oder Ressourcenverlust 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 Kontrollfluss verursachen, der das Ende der Methode bewirkt. Dies bedeutet, dass die Bereinigung mithilfe der Schlüsselwörter 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.

Scott Langham
quelle
6
Sie haben gerade herausgefunden, warum Ausnahmen UglyAndEvil [tm] sind ... ;-) Ausnahmen sind Gotos in ausgefallener, teurer Verkleidung.
EricSchaefer
16
@Eric Sie müssen außergewöhnlich schlechtem Code ausgesetzt gewesen sein. Es ist ziemlich offensichtlich, wenn sie falsch verwendet werden, und sie ermöglichen es Ihnen im Allgemeinen, Code mit höherer Qualität zu schreiben.
Robert Paulson
Dies ist die Antwort, nach der ich wirklich gesucht habe! Hier gibt es gute Antworten, aber die meisten konzentrieren sich auf Beispiele, nicht auf den tatsächlichen Grund, warum diese Empfehlung geboren wurde.
Gustavo Mori
Dies ist die beste Antwort, da sie erklärt, warum dieses ganze Argument überhaupt entstanden ist.
Buttle Butkus
If you've got deep nesting, maybe your function is trying to do too many things.=> Dies ist keine korrekte Konsequenz aus Ihrem vorherigen Satz. Denn kurz bevor Sie sagen, dass Sie Verhalten A mit Code C in Verhalten A mit Code D umgestalten können, ist Code D zwar sauberer, aber "zu viele Dinge" beziehen sich auf Verhalten, das sich NICHT geändert hat. Daher machen Sie mit dieser Schlussfolgerung keinen Sinn.
v.oddou
30

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

Ich hasse es, Code zu lesen, wo es am Anfang zwei Code-Bildschirme gibt und sonst nichts. Einfach umkehren, wenn und zurück. Auf diese Weise verschwendet niemand Zeit mit dem Scrollen.

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

Piotr Perak
quelle
2
Genau. Es ist eher ein Refactoring. Es erleichtert das Lesen des Codes, wie Sie erwähnt haben.
Rpattabi
'Rückkehr' ist nicht genug und schrecklich für eine Wachklausel. Ich würde tun: wenn (ex <= 0) WrongParamValueEx ("[MethodName] Ungültiger Eingabeparameter 1 Wert {0} ... werfen ... und Sie müssen die Ausnahme abfangen und in Ihr App-Protokoll schreiben
JohnJohnGa
3
@John. Sie haben Recht, wenn dies tatsächlich ein Fehler ist. Aber oft ist es nicht. Und anstatt an jeder Stelle, an der die Methode Methode heißt, etwas zu überprüfen, wird es einfach überprüft und es wird nichts zurückgegeben.
Piotr Perak
3
Ich würde es hassen, eine Methode zu lesen, die zwei Bildschirme mit Code, Punkt, ifs oder nein enthält. lol
jpmc26
1
Ich persönlich bevorzuge aus genau diesem Grund eine frühzeitige Rückkehr (auch: Vermeidung von Verschachtelungen). Dies hat jedoch nichts mit der ursprünglichen Frage zur Leistung zu tun und sollte wahrscheinlich ein Kommentar sein.
Patrick M
22

Dies wirkt sich nicht nur auf die Ästhetik aus, sondern verhindert auch das Verschachteln von Code.

Es kann tatsächlich als Voraussetzung dafür dienen, dass auch Ihre Daten gültig sind.

Rion Williams
quelle
18

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 sie conditionhält.
  • Es hält die Verschachtelung niedrig. Das Verschachteln beeinträchtigt die Lesbarkeit mehr als Sie denken.
Deestan
quelle
15

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 Speicherbereinigung wird die try| finallyKonstrukt und usingBlöcke, es gibt wirklich keinen Grund, warum Sie Angst vor ihnen haben sollten.

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

Richard Poole
quelle
Das ist nicht der einzige Grund. Es gibt einen akademischen Grund, der sich auf Pseudocode bezieht und nichts mit praktischen Überlegungen wie dem Reinigen von Sachen zu tun hat. Dieser akamedische Grund hat mit der Achtung der Grundformen imperativer Konstrukte zu tun. Auf die gleiche Weise setzen Sie keine Loop-Exiter in die Mitte. Auf diese Weise können Invarianten rigoros erkannt und das Verhalten nachgewiesen werden. Oder eine Kündigung kann nachgewiesen werden.
v.oddou
1
Nachrichten: Eigentlich habe ich einen sehr praktischen Grund gefunden, warum frühe Pausen und Renditen schlecht sind, und es ist eine direkte Folge meiner statischen Analyse. Los geht's, Intel C ++ - Compiler-Handbuch: d3f8ykwhia686p.cloudfront.net/1live/intel/… . Schlüsselbegriff:a loop that is not vectorizable due to a second data-dependent exit
v.oddou
12

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

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

Es gibt konkurrierende Denkschulen, welcher Ansatz vorzuziehen ist.

Eine Ansicht ist die, die andere erwähnt haben: Der zweite Ansatz reduziert die Verschachtelungsebene, wodurch die Codeklarheit verbessert wird. Dies ist in einem imperativen Stil natürlich: 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 Ausdruck. Also, wenn Anweisungen immer eine else-Klausel haben müssen. Andernfalls hätte der if-Ausdruck nicht immer einen Wert. Im funktionalen Stil ist der erste Ansatz also natürlicher.

Jeffrey Sax
quelle
11

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 wirklich nur an einem Ergebnis einer ifAussage interessiert sind . Also anstatt zu sagen:

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

Sie kehren die Bedingung um und brechen, wenn diese umgekehrte 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

returnist 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 argumentieren, das erste sei sauberer, aber natürlich völlig subjektiv. Einige Programmierer möchten gerne wissen, unter welchen Bedingungen etwas durch Einrücken arbeitet, während ich den Methodenfluss lieber linear halten möchte.

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

Oli
quelle
6
Ich bin wohl einer der wenigen. Ich finde es einfacher, die erste Version zu lesen. Die verschachtelten ifs machen den Entscheidungsbaum offensichtlicher. Auf der anderen Seite, wenn es ein paar Voraussetzungen gibt, stimme ich zu, dass es besser ist, sie alle an die Spitze der Funktion zu setzen.
Anders als
@Otherside: stimme voll und ganz zu. Aufgrund der seriell geschriebenen Rückgaben muss Ihr Gehirn mögliche Pfade serialisieren. Wenn der if-Baum direkt in eine transizente Logik abgebildet werden kann, könnte er beispielsweise zu lisp kompiliert werden. Für die serielle Rückgabe wäre jedoch ein Compiler erforderlich, der viel schwieriger zu handhaben ist. Der Punkt hier hat offensichtlich nichts mit diesem hypothetischen Szenario zu tun, sondern mit Codeanalyse, Optimierungschancen, Korrekturnachweisen, Beendigungsnachweisen und der Erkennung von Invarianten.
v.oddou
1
Niemand findet es einfacher, Code mit mehr Nestern zu lesen. Je mehr Block wie etwas ist, desto einfacher ist es, auf einen Blick zu lesen. Das erste Beispiel hier ist auf keinen Fall einfacher zu lesen und es geht nur in 2 Nester, wenn der meiste Code wie dieser in der realen Welt tiefer geht und mit jedem Nest mehr Gehirnleistung benötigt, um zu folgen.
user441521
1
Wenn die Verschachtelung doppelt so tief wäre, wie lange würden Sie brauchen, um die Frage zu beantworten: "Wann tun Sie etwas anderes?" Ich denke, es würde Ihnen schwer fallen, ohne Stift und Papier zu antworten. Aber man könnte es ziemlich leicht beantworten, wenn alles mit Wachen versehen wäre.
David Storfer
9

Hier werden mehrere gute Punkte gemacht, aber mehrere Rückgabepunkte können auch 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.

Jon Limjap
quelle
8

Die Leistung besteht aus zwei Teilen. Sie haben Leistung, wenn die Software in Produktion ist, aber Sie möchten auch Leistung beim Entwickeln und Debuggen haben. 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 korrekt. Anstatt ifAnweisungen zu verschachteln und einen neuen Bereich im Code zu erstellen, legen Sie zu Beginn Ihrer Methode eine klare Regel fest. Es erhöht die Lesbarkeit, ist einfacher zu warten und reduziert die Anzahl der Regeln, die durchgesehen werden müssen, um herauszufinden, wohin sie wollen.

Ryan Ternier
quelle
7

Persönlich bevorzuge ich nur 1 Ausstiegspunkt. Es ist einfach zu erreichen, wenn Sie Ihre Methoden kurz und auf den Punkt halten, und es bietet 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 innerhalb einer Funktion überprüfen möchten, bevor sie beendet wird. Alles, was Sie tun müssen, ist, einen Haltepunkt bei der endgültigen Rückkehr zu setzen, und Sie werden ihn garantiert erreichen (es sei denn, eine Ausnahme wird ausgelöst).

ilitirit
quelle
4
bool PerformDefaultOperation () {DataStructure defaultParameters = this.GetApplicationDefaults (); return (defaultParameters! = NULL && this.DoSomething (defaultParameters);} Dort wurde es für Sie behoben. :)
tchen
1
Dieses Beispiel ist sehr einfach und verfehlt den Punkt dieses Musters. Haben Sie ungefähr 4 andere Überprüfungen innerhalb dieser Funktion und sagen Sie uns dann, dass sie besser lesbar ist, weil dies nicht der Fall ist.
user441521
5

Viele gute Gründe, wie der Code aussieht . Aber was ist mit den Ergebnissen ?

Schauen wir uns einen C # -Code und seine IL-kompilierte Form an:


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 überprüfen, was das Ergebnis ist. 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, springt zu dem Code, in dem sich die zweite befindet.
  2. Wenn es wahr ist, springt man 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 zur Console.WriteLine, wenn sie falsch ist, oder zum Ende, wenn dies wahr ist.
  4. Drucken Sie die Nachricht aus und kehren Sie zurück.

Es scheint also, dass der Code bis zum Ende springen wird. Was ist, wenn wir ein normales 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 false, gehe zum nächsten Code, wenn true, gehe zum Ende. Und jetzt fließt der IL-Code besser und hat 3 Sprünge (der Compiler hat dies ein wenig optimiert): 1. Erster Sprung: Wenn die Länge 0 ist, zu einem Teil, an dem der Code erneut springt (dritter Sprung) bis zum Ende. 2. Zweitens: 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.

graffic
quelle
5
Dies ist eine gute Information - aber es könnte mich persönlich nicht weniger interessieren, ob IL "besser fließt", weil Leute, die den Code verwalten, keine IL sehen werden
JohnIdol
1
Sie können wirklich nicht vorhersehen, wie der Optimierungsdurchlauf im nächsten Update des C # -Compilers im Vergleich zu dem, was Sie heute sehen, funktionieren wird. Persönlich würde ich KEINE Zeit damit verbringen, den C # -Code zu optimieren, um ein anderes Ergebnis in der IL zu erzielen, es sei denn, die Prüfung zeigt, dass Sie irgendwo in einer engen Schleife ein SEVERE-Leistungsproblem haben und keine anderen Ideen mehr haben . Selbst dann würde ich mir mehr Gedanken über andere Optionen machen. Aus dem gleichen Grund bezweifle ich, dass Sie eine Vorstellung davon haben, welche Arten von Optimierungen der JIT-Compiler mit der IL vornimmt, die ihm für jede Plattform zugeführt wird ...
Craig
5

Theoretisch ifkönnte das Invertieren zu einer besseren Leistung führen, wenn es die Trefferquote für die Verzweigungsvorhersage erhöht. In der Praxis ist es meiner Meinung nach sehr schwierig, genau zu wissen, wie sich die Verzweigungsvorhersage verhält, insbesondere nach dem Kompilieren. Daher würde ich dies in meiner täglichen Entwicklung nicht tun, außer wenn ich Assembler-Code schreibe.

Mehr zur Verzweigungsvorhersage hier .

jfg956
quelle
4

Das ist einfach umstritten. In der Frage der vorzeitigen Rückkehr besteht keine "Einigung zwischen Programmierern". Soweit ich weiß, ist es immer subjektiv.

Es ist möglich, ein Leistungsargument zu liefern, da es besser ist, Bedingungen zu haben, die so geschrieben sind, dass sie meistens wahr sind. 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 werden.

entspannen
quelle
Ich verstehe Ihr Leistungsargument nicht. Die Bedingungen sind boolesch. Unabhängig vom Ergebnis gibt es immer zwei Ergebnisse. Ich verstehe nicht, warum das Umkehren einer Anweisung (oder nicht) ein Ergebnis ändern würde. (Es sei denn, Sie sagen, das Hinzufügen eines "NICHT" zu der Bedingung fügt eine messbare Menge an Verarbeitung hinzu ...
Oli
2
Die Optimierung funktioniert folgendermaßen: Wenn Sie sicherstellen, dass der häufigste Fall im if-Block anstelle des else-Blocks liegt, hat die CPU normalerweise bereits die Anweisungen aus dem if-Block in ihrer Pipeline geladen. Wenn die Bedingung false zurückgibt, muss die CPU ihre Pipeline leeren und ...
Otherside
Ich mache auch gerne das, was andere nur aus Gründen der Lesbarkeit sagen. Ich hasse es, negative Fälle im "Dann" -Block zu haben und nicht im "Sonst". Es ist sinnvoll, dies auch ohne zu wissen oder zu überlegen, was die CPU tut
Andrew Bullock
3

Es gibt dort bereits viele aufschlussreiche Antworten, aber dennoch möchte ich auf eine etwas andere Situation hinweisen: Anstelle einer Vorbedingung, die tatsächlich über eine Funktion gestellt werden sollte, denken Sie an eine schrittweise Initialisierung, 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 unlesbar, da ich dem Verschachtelungsparadigma folgte. Es war ungefähr acht Ebenen tief und ich kann dort keinen Designfehler sehen, wie oben von Andrew Bullock erwähnt. Natürlich hätte ich inneren Code in eine andere Funktion packen und dann die verbleibenden Ebenen dort verschachteln können, um sie besser lesbar zu machen, aber das scheint mir eher zufällig zu sein.

Indem ich die Verschachtelung durch Schutzklauseln ersetzte, entdeckte ich sogar ein Missverständnis von mir in Bezug auf einen Teil des Bereinigungscodes, der innerhalb der Funktion viel früher als am Ende hätte auftreten sollen. Bei 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 Wenns zu einem klareren Code beitragen können.

Ingo Schalk-Schupp
quelle
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) von einem einzigen Exit-Punkt ab. Durch diese Optimierung wird vermieden, dass Ihr Rückgabewert durch Kopieren erstellt wird (in Ihrem speziellen Beispiel spielt dies keine Rolle). Dies kann zu erheblichen Leistungssteigerungen in engen Schleifen führen, da Sie bei jedem Aufruf der Funktion einen Konstruktor und einen Destruktor speichern.

In 99% der Fälle ist das Speichern der zusätzlichen Konstruktor- und Destruktoraufrufe jedoch nicht den Verlust der Lesbarkeit wert, den verschachtelte ifBlöcke verursachen (wie andere bereits betont haben).

Shibumi
quelle
2
Dort. Ich brauchte drei lange Lesungen von zehn Antworten in drei Fragen, in denen ich dasselbe fragte (von denen zwei eingefroren waren), um endlich jemanden zu finden, der NRVO erwähnte. gee ... danke.
v.oddou
2

Es ist Ansichtssache.

Mein normaler Ansatz wäre es, einzeilige Wenns zu vermeiden und mitten in einer Methode zurückzukehren.

Sie möchten keine Zeilen, wie sie überall in Ihrer Methode vorgeschlagen werden, 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 alle erfolgreich sind.

Colin Pickard
quelle
2

Meiner Meinung nach ist eine frühzeitige Rückgabe in Ordnung, wenn Sie nur void zurückgeben (oder einen nutzlosen Rückgabecode, den Sie nie überprüfen werden), und dies könnte die Lesbarkeit verbessern, da Sie das Verschachteln vermeiden und gleichzeitig explizit angeben, dass Ihre Funktion ausgeführt ist.

Wenn Sie tatsächlich einen returnValue zurückgeben, ist die Verschachtelung normalerweise der bessere Weg, da Sie Ihren returnValue nur an einer Stelle (am Ende - duh) zurückgeben und Ihr Code in vielen Fällen möglicherweise besser gewartet werden kann.

JohnIdol
quelle
1

Ich bin mir nicht sicher, aber ich denke, dass R # versucht, Weitsprünge zu vermeiden. Wenn Sie IF-ELSE haben, führt der Compiler Folgendes aus:

Bedingung false -> Weitsprung zu false_condition_label

true_condition_label: Anweisung1 ... Anweisung_n

false_condition_label: Anweisung1 ... Anweisung_n

Endblock

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 Weitsprünge in Kurzsprünge und in diesem Fall besteht eine größere Wahrscheinlichkeit, dass sich alle Anweisungen bereits im Cache befinden.

Marcin Gołembiewski
quelle
0

Ich denke, es hängt davon ab, was Sie bevorzugen, wie erwähnt, es gibt keine allgemeine Übereinstimmung afaik. Um die Belästigung zu verringern, können Sie diese Art von Warnung auf "Hinweis" reduzieren.

Bluenuance
quelle
0

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

    Funktion do_something (Daten) {

      if (! is_valid_data (data)) 
            falsch zurückgeben;


       do_something_that_take_an_hour (Daten);

       istance = new object_with_very_painful_constructor (data);

          if (istance ist ungültig) {
               Fehlermeldung( );
                Rückkehr ;

          }}
       connect_to_database ();
       get_some_other_data ();
       Rückkehr;
    }}

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.

Joshi Spawnbrood
quelle
Mit einer Ausnahmesprache haben Sie diese Probleme nicht.
user441521
0

Diese Art der Codierung bietet 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 der Voraussetzung X schnell mit einem Fehler zurückkehren kann. Dadurch werden zuerst die Fehlerfälle beseitigt und die Komplexität Ihres Codes verringert. In vielen Fällen können Pipeline-Abstürze oder -Schalter gestoppt werden, da die CPU-Pipeline jetzt sauberer sein kann. Zweitens, wenn Sie sich in einer Schleife befinden, können Sie durch schnelles Brechen oder Zurückkehren viel CPU sparen. Einige Programmierer verwenden Schleifeninvarianten, um diese Art von schnellem Beenden durchzuführen. In diesem Fall können Sie jedoch Ihre CPU-Pipeline unterbrechen und sogar Probleme mit der Speichersuche verursachen. Dies bedeutet, dass die CPU aus dem externen Cache geladen werden muss. Aber im Grunde denke ich, du solltest tun, was du beabsichtigt hast, Das heißt, das Ende der Schleife oder Funktion erstellt keinen komplexen Codepfad, nur um eine abstrakte Vorstellung von korrektem Code zu implementieren. Wenn das einzige Werkzeug, das Sie haben, ein Hammer ist, sieht alles aus wie ein Nagel.

David Allan Finch
quelle
Wenn man die Antwort von graffic liest, klingt es eher so, als ob der verschachtelte Code vom Compiler so optimiert wurde, dass der ausgeführte Code schneller ist als die Verwendung mehrerer Rückgaben. Selbst wenn mehrere Rückgaben schneller wären, würde diese Optimierung Ihre Anwendung wahrscheinlich nicht so sehr beschleunigen ... :)
Hangy
1
Ich stimme nicht zu - Beim ersten Gesetz geht es darum, den Algorithmus korrekt zu machen. Aber wir sind alle Ingenieure, bei denen es darum geht, das richtige Problem mit den uns zur Verfügung stehenden Ressourcen zu lösen und das verfügbare Budget einzuhalten. Eine zu langsame Anwendung ist nicht zweckmäßig.
David Allan Finch