it-swarm-eu.dev

Woher kommt der Begriff "nur eine Rückkehr"?

Ich spreche oft mit Programmierern, die sagen: "Setzen Sie nicht mehrere return-Anweisungen in dieselbe Methode." Wenn ich sie auffordere, mir die Gründe dafür zu nennen, bekomme ich nur "Die Codierung Standard sagt es. "oder" Es ist verwirrend. "Wenn sie mir Lösungen mit einer einzigen return-Anweisung zeigen, sieht der Code für mich hässlicher aus. Zum Beispiel:

if (condition)
   return 42;
else
   return 97;

"Das ist hässlich, du musst eine lokale Variable verwenden!"

int result;
if (condition)
   result = 42;
else
   result = 97;
return result;

Wie macht dieses Aufblähen von 50% Code das Programm leichter verständlich? Persönlich finde ich es schwieriger, weil der Zustandsraum gerade um eine andere Variable vergrößert wurde, die leicht hätte verhindert werden können.

Normalerweise würde ich normalerweise nur schreiben:

return (condition) ? 42 : 97;

Viele Programmierer meiden jedoch den bedingten Operator und bevorzugen die lange Form.

Woher kommt dieser Begriff "nur eine Rückkehr"? Gibt es einen historischen Grund, warum diese Konvention zustande kam?

1077
fredoverflow

"Single Entry, Single Exit" wurde geschrieben, als die meisten Programmierungen in Assemblersprache, FORTRAN oder COBOL durchgeführt wurden. Es wurde weitgehend falsch interpretiert, weil moderne Sprachen die Praktiken, vor denen Dijkstra gewarnt hatte, nicht unterstützen.

"Einzeleintrag" bedeutet "Keine alternativen Einstiegspunkte für Funktionen erstellen". In der Assemblersprache ist es natürlich möglich, bei jeder Anweisung eine Funktion einzugeben. FORTRAN unterstützte mehrere Einträge für Funktionen mit der Anweisung ENTRY:

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

"Single Exit" bedeutete, dass eine Funktion nur to eine Stelle zurückgeben sollte: die Anweisung unmittelbar nach dem Aufruf. Es bedeutete nicht, dass eine Funktion nur von einer Stelle zurückgeben sollte. Wenn Structured Programming geschrieben wurde, war es üblich, dass eine Funktion einen Fehler anzeigt, indem sie an einen anderen Ort zurückkehrt. FORTRAN unterstützte dies über "alternative Rückgabe":

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

Beide Techniken waren sehr fehleranfällig. Durch die Verwendung alternativer Einträge wurde eine Variable häufig nicht initialisiert. Die Verwendung alternativer Rückgaben hatte alle Probleme einer GOTO-Anweisung, mit der zusätzlichen Komplikation, dass die Verzweigungsbedingung nicht neben der Verzweigung, sondern irgendwo in der Unterroutine lag.

1145
kevin cline

Dieser Begriff von Single Entry, Single Exit (SESE) stammt aus Sprachen mit expliziter Ressourcenverwaltung, wie C und Assembly. In C werden durch Code wie diesen Ressourcen verloren gehen:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

In solchen Sprachen haben Sie grundsätzlich drei Möglichkeiten:

  • Replizieren Sie den Bereinigungscode.
    Pfui. Redundanz ist immer schlecht.

  • Verwenden Sie ein goto, um zum Bereinigungscode zu springen.
    Dies erfordert, dass der Bereinigungscode das Letzte in der Funktion ist. (Und deshalb argumentieren einige, dass goto seinen Platz hat. Und es hat tatsächlich - in C.)

  • Führen Sie eine lokale Variable ein und manipulieren Sie den Kontrollfluss durch diese.
    Der Nachteil ist, dass der durch die Syntax manipulierte Kontrollfluss (denken Sie an break, return, if, while) viel einfacher zu befolgen ist als Kontrollfluss, der durch den Status von Variablen manipuliert wird (da diese Variablen beim Betrachten des Algorithmus keinen Status haben).

In Assembly ist es noch seltsamer, weil Sie zu jeder Adresse in einer Funktion springen können, wenn Sie diese Funktion aufrufen, was effektiv bedeutet, dass Sie eine nahezu unbegrenzte Anzahl von Einstiegspunkten für jede Funktion haben. (Manchmal ist dies hilfreich. Solche Thunks sind eine gängige Technik für Compiler, um die Zeigeranpassung this zu implementieren, die zum Aufrufen von virtual -Funktionen in Szenarien mit Mehrfachvererbung in C++ erforderlich ist.)

Wenn Sie Ressourcen manuell verwalten müssen, führt das Ausnutzen der Optionen zum Eingeben oder Verlassen einer Funktion an einer beliebigen Stelle zu komplexerem Code und damit zu Fehlern. Daher erschien eine Denkschule, die SESE propagierte, um saubereren Code und weniger Fehler zu erhalten.


Wenn eine Sprache jedoch Ausnahmen enthält, kann (fast) jede Funktion zu (fast) jedem Zeitpunkt vorzeitig beendet werden, sodass Sie ohnehin Vorkehrungen für eine vorzeitige Rückgabe treffen müssen. (Ich denke, finally wird hauptsächlich dafür in Java und using verwendet (bei der Implementierung von IDisposable, finally ansonsten) in C #; C++ verwendet stattdessen RAII .) Sobald Sie dies getan haben, können Sie nicht nicht nach sich selbst aufräumen zu einer frühen return Aussage, also ist das wahrscheinlich stärkste Argument für SESE verschwunden.

Das lässt Lesbarkeit. Natürlich ist eine 200-LoC-Funktion mit einem halben Dutzend return -Anweisungen, die zufällig darüber gestreut werden, kein guter Programmierstil und sorgt nicht für lesbaren Code. Aber eine solche Funktion wäre auch ohne diese vorzeitigen Rückgaben nicht leicht zu verstehen.

In Sprachen, in denen Ressourcen nicht manuell verwaltet werden oder werden sollten, ist die Einhaltung der alten SESE-Konvention wenig oder gar nicht sinnvoll. OTOH, wie ich oben dargelegt habe, SESE macht Code oft komplexer. Es ist ein Dinosaurier, der (mit Ausnahme von C) nicht gut in die meisten heutigen Sprachen passt. Anstatt die Verständlichkeit von Code zu verbessern, behindert er ihn.


Warum halten sich Java Programmierer daran? Ich weiß es nicht, aber von meinem (externen) POV hat Java hat viele Konventionen von C übernommen (wo) sie machen Sinn) und wendeten sie auf ihre OO Welt) an (wo sie nutzlos oder geradezu schlecht sind), wo sie jetzt an ihnen haftet, unabhängig von den Kosten. (Wie die Konvention, alle zu definieren Ihre Variablen am Anfang des Bereichs.)

Programmierer halten sich aus irrationalen Gründen an alle möglichen seltsamen Notationen. (Tief verschachtelte strukturelle Aussagen - "Pfeilspitzen" - wurden in Sprachen wie Pascal einst als schöner Code angesehen.) Die Anwendung rein logischer Argumentation scheint die Mehrheit von ihnen nicht davon zu überzeugen, von ihren etablierten Methoden abzuweichen. Der beste Weg, solche Gewohnheiten zu ändern, besteht wahrscheinlich darin, sie frühzeitig zu lehren, das Beste zu tun, nicht das, was konventionell ist. Als Programmierlehrer haben Sie es in der Hand. :)

921
sbi

Einerseits erleichtern einzelne return-Anweisungen die Protokollierung sowie Debugging-Formen, die auf der Protokollierung beruhen. Ich erinnere mich, dass ich die Funktion oft auf eine einzelne Rückgabe reduzieren musste, um den Rückgabewert an einem einzelnen Punkt auszudrucken.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

Andererseits können Sie dies in function() umgestalten, das _function() aufruft und das Ergebnis protokolliert.

82
perreal

"Single Entry, Single Exit" entstand mit der Strukturierten Programmierrevolution der frühen 1970er Jahre, die durch Edsger W. Dijkstras Brief an den Herausgeber " GOTO Statement Considered Harmful " eingeleitet wurde. Die Konzepte der strukturierten Programmierung wurden im klassischen Buch "Strukturierte Programmierung" von Ole Johan-Dahl, Edsger W. Dijkstra und Charles Anthony Richard Hoare ausführlich dargelegt.

"GOTO Statement Considered Harmful" muss auch heute noch gelesen werden. "Strukturierte Programmierung" ist veraltet, aber immer noch sehr, sehr lohnend und sollte ganz oben auf der "Must Read" -Liste eines Entwicklers stehen, weit über allem, was z. Steve McConnell. (In Dahls Abschnitt werden die Grundlagen der Klassen in Simula 67 erläutert, die die technische Grundlage für Klassen in C++ und die gesamte objektorientierte Programmierung bilden.)

53
John R. Strohm

Es ist immer einfach, Fowler zu verlinken.

Eines der Hauptbeispiele gegen SESE sind Schutzklauseln:

Verschachtelte Bedingung durch Schutzklauseln ersetzen

Verwenden Sie für alle Sonderfälle Schutzklauseln

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

 http://www.refactoring.com/catalog/arrow.gif

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

Weitere Informationen finden Sie auf Seite 250 von Refactoring ...

36
Pieter B

Ich habe vor einiger Zeit einen Blog-Beitrag zu diesem Thema geschrieben.

Das Fazit ist, dass diese Regel aus dem Zeitalter von Sprachen stammt, die keine Speicherbereinigung oder Ausnahmebehandlung haben. Es gibt keine formale Studie, die zeigt, dass diese Regel zu einem besseren Code in modernen Sprachen führt. Sie können es jederzeit ignorieren, wenn dies zu kürzerem oder besser lesbarem Code führt. Die Java Leute, die darauf bestehen, sind blind und fraglos und folgen einer veralteten, sinnlosen Regel.

Diese Frage wurde auch bei Stackoverflow gestellt

11
Anthony

Eine Rückgabe erleichtert das Refactoring. Versuchen Sie, eine "Extraktionsmethode" für den inneren Körper einer for-Schleife durchzuführen, die eine Rückgabe, Unterbrechung oder Fortsetzung enthält. Dies schlägt fehl, da Sie Ihren Kontrollfluss unterbrochen haben.

Der Punkt ist: Ich denke, dass niemand vorgibt, perfekten Code zu schreiben. Daher wird Code regelmäßig überarbeitet, um "verbessert" und erweitert zu werden. Mein Ziel wäre es also, meinen Code so refactoring-freundlich wie möglich zu halten.

Oft stehe ich vor dem Problem, dass ich Funktionen komplett neu formulieren muss, wenn sie Steuerflussunterbrecher enthalten und ich nur wenig Funktionalität hinzufügen möchte. Dies ist sehr fehleranfällig, da Sie den gesamten Kontrollfluss ändern, anstatt neue Pfade zu isolierten Verschachtelungen einzuführen. Wenn Sie am Ende nur eine einzige Rückkehr haben oder wenn Sie Wachen verwenden, um eine Schleife zu verlassen, haben Sie natürlich mehr Verschachtelung und mehr Code. Sie erhalten jedoch Compiler- und IDE unterstützte Refactoring-Funktionen).

6
oopexpert

Berücksichtigen Sie die Tatsache, dass mehrere return-Anweisungen gleichbedeutend sind mit GOTOs zu einer einzelnen return-Anweisung. Dies ist auch bei break-Anweisungen der Fall. Einige, wie ich, betrachten sie daher in jeder Hinsicht als GOTOs.

Ich halte diese Arten von GOTOs jedoch nicht für schädlich und zögere nicht, ein tatsächliches GOTO in meinem Code zu verwenden, wenn ich einen guten Grund dafür finde.

Meine allgemeine Regel ist, dass GOTOs nur zur Flusskontrolle dienen. Sie sollten niemals für Schleifen verwendet werden, und Sie sollten niemals "nach oben" oder "rückwärts" gehen. (So ​​funktionieren Pausen/Rückgaben)

Wie andere bereits erwähnt haben, ist das Folgende ein Muss GOTO-Erklärung als schädlich eingestuft
Denken Sie jedoch daran, dass dies 1970 geschrieben wurde, als GOTOs viel zu häufig verwendet wurden. Nicht jedes GOTO ist schädlich und ich würde ihre Verwendung nicht entmutigen, solange Sie sie nicht anstelle normaler Konstrukte verwenden, sondern in dem seltsamen Fall, dass die Verwendung normaler Konstrukte äußerst unpraktisch wäre.

Ich finde, dass die Verwendung in Fehlerfällen, in denen Sie aufgrund eines Fehlers aus einem Bereich entkommen müssen, der in normalen Fällen manchmal nie auftreten sollte, manchmal nützlich ist. Sie sollten diesen Code aber auch in eine separate Funktion einfügen, damit Sie einfach frühzeitig zurückkehren können, anstatt ein GOTO zu verwenden ... aber manchmal ist das auch unpraktisch.

5
user606723

Zyklomatische Komplexität

Ich habe gesehen, dass SonarCube mehrere return-Anweisungen zur Bestimmung der zyklomatischen Komplexität verwendet. Je mehr die return-Anweisungen, desto höher die zyklomatische Komplexität

Änderung des Rückgabetyps

Mehrere Rückgaben bedeuten, dass wir an mehreren Stellen in der Funktion Änderungen vornehmen müssen, wenn wir unseren Rückgabetyp ändern möchten.

Mehrfachausgang

Das Debuggen ist schwieriger, da die Logik in Verbindung mit den bedingten Anweisungen sorgfältig untersucht werden muss, um zu verstehen, was den zurückgegebenen Wert verursacht hat.

Überarbeitete Lösung

Die Lösung für mehrere Rückgabeanweisungen besteht darin, sie durch Polymorphismus zu ersetzen und nach dem Auflösen des erforderlichen Implementierungsobjekts eine einzige Rückgabe zu erhalten.

4
Sorter