Ich spreche oft mit Programmierern, die sagen: " Setzen Sie nicht mehrere return-Anweisungen in dieselbe Methode ein. " Wenn ich sie auffordern, mir die Gründe dafür zu nennen, bekomme ich nur " Der Codierungsstandard sagt das " oder " Es ist verwirrend ". Wenn sie mir Lösungen mit einer einzelnen return-Anweisung zeigen, erscheint mir der Code hässlicher. 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 wird das Programm durch diesen zu 50% aufgeblähten Code verständlicher? 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 natürlich nur schreiben:
return (condition) ? 42 : 97;
Viele Programmierer meiden jedoch den bedingten Operator und bevorzugen die lange Form.
Woher kommt diese Vorstellung von "nur einer Rückkehr"? Gibt es einen historischen Grund, warum diese Konvention zustande kam?
Antworten:
"Single Entry, Single Exit" wurde geschrieben, als die meisten Programmierungen in Assembler, FORTRAN oder COBOL durchgeführt wurden. Es wurde weitestgehend falsch interpretiert, da moderne Sprachen die Praktiken, vor denen Dijkstra warnte, nicht unterstützen.
"Einzelner Eintrag" bedeutet "keine alternativen Einstiegspunkte für Funktionen erstellen". In der Assemblersprache ist es natürlich möglich, an jeder Anweisung eine Funktion einzugeben. FORTRAN unterstützte mehrere Einträge zu Funktionen mit der
ENTRY
Anweisung:"Single Exit" bedeutete, dass eine Funktion nur an eine Stelle zurückkehren sollte: die Anweisung, die unmittelbar auf den Aufruf folgt. Es bedeutete nicht , dass eine Funktion nur von einer Stelle zurückkehren sollte. Beim Schreiben von Structured Programming war es üblich, dass eine Funktion einen Fehler anzeigt, indem sie an einen anderen Ort zurückkehrt. FORTRAN hat dies über "Alternate Return" unterstützt:
Beide Techniken waren sehr fehleranfällig. Die Verwendung von alternativen Einträgen ließ häufig eine Variable uninitialisiert. Die Verwendung alternativer Rückgaben hatte alle Probleme einer GOTO-Anweisung mit der zusätzlichen Komplikation, dass die Verzweigungsbedingung nicht an die Verzweigung angrenzte, sondern sich irgendwo in der Unterroutine befand.
quelle
const
seitdem, bevor viele der Benutzer hier geboren wurden, keine Notwendigkeit mehr für Kapitalkonstanten Aber Java hat all diese schlechten alten C-Gewohnheiten bewahrt .setjmp/longjmp
?)Dieser Begriff von Single Entry, Single Exit (SESE) stammt aus Sprachen mit expliziter Ressourcenverwaltung wie C und Assembly. In C verliert Code wie dieser Ressourcen:
In solchen Sprachen haben Sie grundsätzlich drei Möglichkeiten:
Replizieren Sie den Bereinigungscode.
Pfui. Redundanz ist immer schlecht.
Verwenden Sie a
goto
, um zum Bereinigungscode zu springen.Dies erfordert, dass der Bereinigungscode das Letzte in der Funktion ist. (Und deshalb argumentieren einige, dass
goto
es seinen Platz hat. Und es hat in der Tat - in C.)Führen Sie eine lokale Variable ein und manipulieren Sie den Steuerungsfluss.
Der Nachteil ist , dass Steuerfluss durch Syntax manipuliert (denken
break
,return
,if
,while
) ist viel einfacher als durch den Zustand der Variablen manipulierte Steuerfluss zu folgen (da diese Variablen haben keinen Zustand , wenn Sie auf dem Algorithmus aussehen).In der Montage ist es noch seltsamer, weil Sie zu jeder Adresse in einer Funktion springen können, wenn Sie diese Funktion aufrufen, was praktisch bedeutet, dass Sie eine nahezu unbegrenzte Anzahl von Eintrittspunkten für jede Funktion haben. (Manchmal ist dies hilfreich. Solche Thunks sind eine übliche Technik für Compiler, um die
this
Zeigeranpassung zu implementieren, die zum Aufrufen vonvirtual
Funktionen in Mehrfachvererbungsszenarios in C ++ erforderlich ist .)Wenn Sie Ressourcen manuell verwalten müssen, führt das Ausnutzen der Optionen zum Aufrufen oder Verlassen einer Funktion zu komplexerem Code und damit zu Fehlern. Daher erschien eine Denkschule, die SESE propagierte, um saubereren Code und weniger Bugs zu erhalten.
Wenn jedoch eine Sprache Ausnahmen aufweist, kann (fast) jede Funktion zu (fast) jedem Zeitpunkt vorzeitig beendet werden. Daher müssen Sie ohnehin Vorkehrungen für eine vorzeitige Rückkehr treffen. (Ich denke, dies
finally
wird hauptsächlich in Java undusing
(bei der ImplementierungIDisposable
,finally
ansonsten) in C # verwendet; C ++ verwendet stattdessen RAII .) Wenn Sie dies getan haben, können Sie es nicht versäumen, aufgrund einer frühenreturn
Aussage nach sich aufzuräumen das stärkste Argument für SESE ist verschwunden.Damit bleibt die Lesbarkeit. Natürlich ist eine 200-LoC-Funktion mit einem halben Dutzend
return
zufällig darüber gestreuten Anweisungen kein guter Programmierstil und ergibt keinen lesbaren Code. Aber eine solche Funktion wäre auch ohne diese vorzeitigen Erträge nicht leicht zu verstehen.In Sprachen, in denen Ressourcen nicht manuell verwaltet werden oder verwaltet werden sollten, ist die Einhaltung der alten SESE-Konvention wenig oder gar nicht sinnvoll. OTOH, wie ich oben dargelegt habe, macht SESE Code oft komplexer . Es ist ein Dinosaurier, der (mit Ausnahme von C) nicht gut in die meisten heutigen Sprachen passt. Anstatt die Verständlichkeit des Codes zu verbessern, behindert es ihn.
Warum halten Java-Programmierer daran fest? Ich weiß es nicht, aber von meinem (externen) POV aus hat Java eine Menge Konventionen von C übernommen (wo sie Sinn machen) und sie auf seine OO-Welt angewendet (wo sie nutzlos oder ausgesprochen schlecht sind), wo sie sich jetzt festhalten sie, egal was die Kosten. (Wie die Konvention, alle Variablen am Anfang des Gültigkeitsbereichs zu definieren.)
Programmierer halten aus irrationalen Gründen an allerlei merkwürdigen Notationen fest. (Tief verschachtelte strukturelle Aussagen - "Pfeilspitzen" - wurden in Sprachen wie Pascal einst als schöner Code angesehen.) Eine logische Argumentation scheint die Mehrheit von ihnen nicht davon zu überzeugen, von ihren etablierten Methoden abzuweichen. Der beste Weg, solche Gewohnheiten zu ändern, ist wahrscheinlich, sie früh zu lehren, das Beste und nicht das Konventionelle zu tun. Als Programmierlehrer haben Sie es in der Hand.
:)
quelle
finally
Klauseln, in denen er unabhängig von frühenreturn
s oder Ausnahmen ausgeführt wird.finally
meiste Zeit.malloc()
undfree()
in einen Kommentar als Beispiel), ich spreche über Ressourcen im Allgemeinen. Ich habe auch nicht impliziert, dass GC diese Probleme lösen würde. (Ich habe C ++ erwähnt, das GC nicht sofort einsatzbereit hat.) Soweit ich weiß, wird Javafinally
zur Lösung dieses Problems verwendet.Einerseits erleichtern einzelne return-Anweisungen die Protokollierung sowie Formen des Debuggens, die auf der Protokollierung beruhen. Ich erinnere mich noch gut daran, wie oft ich die Funktion auf Einzelrückgabe reduzieren musste, um den Rückgabewert an einer Stelle auszudrucken.
Auf der anderen Seite können Sie dies in
function()
die Aufrufe umgestalten_function()
und das Ergebnis protokollieren.quelle
_function()
mitreturn
s an geeigneten Stellen und einem Wrapperfunction()
, der die externe Protokollierung übernimmt, als einen Codefunction()
mit verzerrter Logik, damit alle Rückgaben in einen einzelnen Exit passen -point, damit ich eine zusätzliche Anweisung vor diesem Punkt einfügen kann."Single Entry, Single Exit" entstand mit der Revolution der strukturierten Programmierung in den frühen 1970er Jahren, die durch den Brief von Edsger W. Dijkstra an den Herausgeber " GOTO Statement Considered Harmful " eingeleitet wurde . Die Konzepte für die strukturierte Programmierung wurden im klassischen Buch "Structured Programming" von Ole Johan-Dahl, Edsger W. Dijkstra und Charles Anthony Richard Hoare ausführlich beschrieben.
"GOTO Statement als schädlich" muss auch heute noch gelesen werden. "Structured Programming" ist veraltet, aber immer noch sehr, sehr lohnend und sollte ganz oben auf der "Must Read" -Liste eines Entwicklers stehen, weit über allem von 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.)
quelle
goto
man buchstäblich überall hingehen konnte , wie an einem zufälligen Punkt in einer anderen Funktion, ohne die Vorstellung von Prozeduren, Funktionen, einem Aufrufstapel usw. Keine vernünftige Sprache erlaubt dies heutzutage mit einem Straightgoto
. Cssetjmp
/longjmp
ist der einzige mir bekannte Ausnahmefall, und selbst das erfordert Kooperation von beiden Seiten. (Halb-ironisch, dass ich dort das Wort "außergewöhnlich" verwendet habe, wenn man bedenkt, dass Ausnahmen fast dasselbe tun ...) Im Grunde rät der Artikel von einer Praxis ab, die schon lange tot ist.Es ist immer einfach, Fowler zu verlinken.
Eines der wichtigsten Beispiele, die gegen SESE sprechen, sind Schutzklauseln:
Ersetzen Sie verschachtelte Bedingungen durch Schutzklauseln
quelle
_isSeparated
und_isRetired
können beide wahr sein (und warum wäre das nicht möglich?), Geben Sie den falschen Betrag zurück.Ich habe vor einiger Zeit einen Blog-Beitrag zu diesem Thema geschrieben.
Die Quintessenz ist, dass diese Regel aus dem Zeitalter von Sprachen stammt, die keine Speicherbereinigung oder Ausnahmebehandlung haben. Es gibt keine offizielle Studie, die belegt, dass diese Regel zu einem besseren Code in modernen Sprachen führt. Sie können es jederzeit ignorieren, wenn dies zu einem kürzeren oder besser lesbaren Code führt. Die Java-Leute, die darauf bestehen, folgen blind und fraglos einer veralteten, sinnlosen Regel.
Diese Frage wurde auch bei Stackoverflow gestellt
quelle
Eine Rückgabe erleichtert das Refactoring. Versuchen Sie, eine "Extraktionsmethode" für den inneren Körper einer for-Schleife auszuführen, die ein return, break oder continue enthält. Dies schlägt fehl, da Sie Ihren Kontrollfluss unterbrochen haben.
Der Punkt ist: Ich denke, niemand gibt vor, 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 umgestaltungsfreundlich wie möglich zu halten.
Oft stehe ich vor dem Problem, dass ich Funktionen komplett neu formulieren muss, wenn sie Kontrollflussunterbrecher enthalten und ich nur wenig Funktionalität hinzufügen möchte. Dies ist sehr fehleranfällig, da Sie den Steuerungsfluss vollständig ändern, anstatt neue Pfade zu isolierten Sammelformen einzufügen. Wenn Sie am Ende nur eine einzige Rückkehr haben oder Wachen zum Verlassen einer Schleife verwenden, haben Sie natürlich mehr Verschachtelung und mehr Code. Sie erhalten jedoch Compiler- und IDE-unterstützte Refactoring-Funktionen.
quelle
Berücksichtigen Sie die Tatsache, dass mehrere return-Anweisungen den GOTOs einer einzelnen return-Anweisung entsprechen. Dies ist auch bei break-Anweisungen der Fall. Daher betrachten manche, wie ich, sie als GOTOs in jeder Hinsicht.
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 "aufwärts" oder "rückwärts" gehen. (So funktionieren Pausen / Retouren)
Wie andere erwähnt haben, ist das Folgende ein Muss GOTO-Erklärung als schädlich zu lesen. Beachten Sie
jedoch, dass dies im Jahr 1970 geschrieben wurde, als GOTOs viel zu stark genutzt wurden. Nicht jedes GOTO ist schädlich und ich würde von ihrer Verwendung nicht abraten, solange Sie sie nicht anstelle normaler Konstrukte verwenden, sondern in dem merkwürdigen Fall, dass die Verwendung normaler Konstrukte äußerst unpraktisch wäre.
Ich finde, dass die Verwendung in Fehlerfällen, in denen Sie einen Bereich aufgrund eines Fehlers verlassen müssen, der im Normalfall nie auftreten sollte, manchmal nützlich ist. Sie sollten aber auch in Betracht ziehen, diesen Code in eine separate Funktion zu integrieren, damit Sie einfach früher zurückkehren können, anstatt ein GOTO zu verwenden ... aber manchmal ist das auch unpraktisch.
quelle
Zyklomatische Komplexität
Ich habe gesehen, dass SonarCube mehrere return-Anweisungen verwendet, um die zyklomatische Komplexität zu bestimmen. Je mehr die return-Anweisungen, desto höher die zyklomatische Komplexität
Rückgabetyp ändern
Mehrere Rückgaben bedeuten, dass wir an mehreren Stellen in der Funktion Änderungen vornehmen müssen, wenn wir uns entscheiden, unseren Rückgabetyp zu ändern.
Multiple Exit
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.
Refactored Lösung
Die Lösung für mehrere return-Anweisungen besteht darin, sie durch polymorphe Anweisungen zu ersetzen, nachdem das erforderliche Implementierungsobjekt aufgelöst wurde.
quelle