Warum sollte eine Funktion nur einen Austrittspunkt haben? [geschlossen]

97

Ich habe immer von einer einzelnen Exit-Point-Funktion als schlechter Weg zum Codieren gehört, weil Sie an Lesbarkeit und Effizienz verlieren. Ich habe noch nie jemanden auf der anderen Seite streiten hören.

Ich dachte, das hätte etwas mit CS zu tun, aber diese Frage wurde bei cstheory stackexchange abgeschossen.

Hex Bob-Omb
quelle
6
Die Antwort ist, dass es keine Antwort gibt, die immer richtig ist. Ich finde es oft einfacher, mit mehreren Exits zu codieren. Ich habe auch festgestellt (beim Aktualisieren des obigen Codes), dass das Ändern / Erweitern des Codes aufgrund derselben mehrfachen Exits schwieriger war. Diese Entscheidungen von Fall zu Fall zu treffen, ist unsere Aufgabe. Wenn eine Entscheidung immer eine "beste" Antwort hat, brauchen wir sie nicht.
JS.
1
@finnw die faschistischen Mods haben die letzten beiden Fragen entfernt, um sicherzustellen, dass sie immer wieder und immer wieder beantwortet werden müssen
Maarten Bodewes
Trotz des Wortes "argumentieren" in der Frage denke ich wirklich nicht, dass dies eine meinungsbasierte Frage ist. Es ist ziemlich relevant für gutes Design usw. Ich sehe keinen Grund dafür, es zu schließen, aber w / e.
Ungeheuer
1
Ein einziger Exit-Punkt vereinfacht das Debuggen, Lesen, Messen und Optimieren der Leistung sowie das Refactoring. Dies ist objektiv und von wesentlicher Bedeutung. Die Verwendung früher Rückgaben (nach einfachen Argumentprüfungen) führt zu einer sinnvollen Mischung beider Stile. Angesichts der Vorteile eines einzelnen Austrittspunkts ist das Verunreinigen Ihres Codes mit Rückgabewerten lediglich ein Beweis für einen faulen, schlampigen, nachlässigen Programmierer - und zumindest möglicherweise nicht für Welpen.
Rick O'Shea

Antworten:

108

Es gibt verschiedene Denkrichtungen, und es kommt größtenteils auf die persönlichen Vorlieben an.

Zum einen ist es weniger verwirrend, wenn es nur einen einzigen Austrittspunkt gibt - Sie haben einen einzigen Pfad durch die Methode und wissen, wo Sie nach dem Ausgang suchen müssen. Auf der negativen Seite, wenn Sie Einrückungen verwenden, um die Verschachtelung darzustellen, wird Ihr Code massiv nach rechts eingerückt, und es wird sehr schwierig, allen verschachtelten Bereichen zu folgen.

Zum anderen können Sie die Voraussetzungen überprüfen und zu Beginn einer Methode frühzeitig beenden, sodass Sie im Hauptteil der Methode wissen, dass bestimmte Bedingungen erfüllt sind, ohne dass der gesamte Körper der Methode 5 Meilen nach rechts eingerückt ist. Dies minimiert normalerweise die Anzahl der Bereiche, über die Sie sich Sorgen machen müssen, was das Verfolgen von Code erheblich erleichtert.

Ein dritter ist, dass Sie jederzeit verlassen können. Früher war dies verwirrender, aber jetzt, da wir Syntax-Editoren und Compiler haben, die nicht erreichbaren Code erkennen, ist es viel einfacher, damit umzugehen.

Ich bin gerade im mittleren Lager. Das Erzwingen eines einzelnen Austrittspunkts ist meiner Meinung nach eine sinnlose oder sogar kontraproduktive Einschränkung, während das zufällige Verlassen einer Methode manchmal zu einer chaotischen, schwer zu befolgenden Logik führen kann, bei der es schwierig wird zu erkennen, ob ein bestimmtes Codebit vorhanden ist oder nicht hingerichtet. Durch das "Gating" Ihrer Methode ist es jedoch möglich, den Hauptteil der Methode erheblich zu vereinfachen.

Jason Williams
quelle
1
Eine tiefe Verschachtelung kann im singe exitParadigma durch go toAussagen vermieden werden . Darüber hinaus hat man die Möglichkeit, eine Nachbearbeitung unter der lokalen ErrorBezeichnung der Funktion durchzuführen , was mit mehreren returns unmöglich ist .
Ant_222
2
Es gibt normalerweise eine gute Lösung, die die Notwendigkeit eines Besuchs vermeidet. Ich ziehe es vor, 'return (Fail (...))' und den gemeinsam genutzten Bereinigungscode in die Fail-Methode einzufügen. Dies erfordert möglicherweise die Übergabe einiger Einheimischer, damit Speicher freigegeben werden kann usw. Wenn Sie sich jedoch nicht in einem leistungskritischen Code befinden, ist dies normalerweise eine viel sauberere Lösung als eine goto IMO. Es ermöglicht auch mehreren Methoden, ähnlichen Bereinigungscode gemeinsam zu nutzen.
Jason Williams
Es gibt einen optimalen Ansatz, der auf objektiven Kriterien basiert, aber wir können uns darauf einigen, dass es Denkschulen gibt (richtig und falsch), und es kommt auf die persönlichen Vorlieben an (eine Präferenz für oder gegen einen richtigen Ansatz).
Rick O'Shea
39

Meine allgemeine Empfehlung lautet, dass return-Anweisungen, wenn dies praktikabel ist, entweder vor dem ersten Code mit Nebenwirkungen oder nach dem letzten Code mit Nebenwirkungen stehen sollten. Ich würde etwas in Betracht ziehen wie:

  if (! Argument) // Überprüfen Sie, ob nicht null
    return ERR_NULL_ARGUMENT;
  ... Nicht-Null-Argument verarbeiten
  falls in Ordnung)
    return 0;
  sonst
    return ERR_NOT_OK;

klarer als:

  int return_value;
  if (Argument) // Nicht null
  {
    .. Nicht-Null-Argument verarbeiten
    .. Ergebnis entsprechend einstellen
  }}
  sonst
    Ergebnis = ERR_NULL_ARGUMENT;
  Ergebnis zurückgeben;

Wenn eine bestimmte Bedingung eine Funktion daran hindern sollte, etwas zu tun, ziehe ich es vor, an einer Stelle oberhalb des Punktes, an dem die Funktion etwas tun würde, frühzeitig aus der Funktion zurückzukehren. Sobald die Funktion Aktionen mit Nebenwirkungen durchgeführt hat, kehre ich lieber von unten zurück, um zu verdeutlichen, dass alle Nebenwirkungen behandelt werden müssen.

Superkatze
quelle
Ihr erstes Beispiel, die Verwaltung der okVariablen, scheint mir der Single-Return-Ansatz zu sein. Darüber hinaus kann der if-else-Block einfach umgeschrieben werden in:return ok ? 0 : ERR_NOT_OK;
Melebius
2
Das erste Beispiel hat returnam Anfang einen Code vor allem, der alles macht. Wenn Sie den ?:Operator in separaten Zeilen schreiben, können viele IDEs leichter einen Debug-Haltepunkt an das nicht in Ordnung befindliche Szenario anhängen. Übrigens liegt der eigentliche Schlüssel zum "einzelnen Austrittspunkt" im Verständnis, dass es darauf ankommt, dass für jeden bestimmten Aufruf einer normalen Funktion der Austrittspunkt der Punkt unmittelbar nach dem Anruf ist . Programmierer halten das heutzutage für selbstverständlich, aber das war nicht immer so. In einigen seltenen Fällen muss Code möglicherweise ohne Stapelspeicher auskommen, was zu Funktionen führt ...
Supercat
... die über bedingte oder berechnete gotos beendet werden. Im Allgemeinen kann alles, was über genügend Ressourcen verfügt, um in einer anderen Sprache als der Assemblersprache programmiert zu werden, einen Stapel unterstützen. Ich habe jedoch Assembler-Code geschrieben, der unter sehr strengen Einschränkungen arbeiten musste (in einem Fall bis auf NULL RAM-Bytes). In solchen Fällen kann es hilfreich sein, mehrere Austrittspunkte zu haben.
Supercat
1
Das so genannte klarere Beispiel ist viel weniger klar und schwer zu lesen. Ein Exit-Punkt ist immer leichter zu lesen, leichter zu warten und leichter zu debuggen.
GuidoG
8
@GuidoG: Jedes Muster ist möglicherweise besser lesbar, je nachdem, was in den ausgelassenen Abschnitten angezeigt wird. Verwenden von "return x;" macht deutlich, dass bei Erreichen der Anweisung der Rückgabewert x ist. Verwenden von "result = x;" lässt die Möglichkeit offen, dass etwas anderes das Ergebnis ändert, bevor es zurückgegeben wird. Dies kann nützlich sein, wenn es tatsächlich notwendig wäre, das Ergebnis zu ändern, aber Programmierer, die den Code untersuchen, müssten ihn überprüfen, um festzustellen, wie sich das Ergebnis ändern könnte, selbst wenn die Antwort "es kann nicht" lautet.
Supercat
15

Ein einziger Ein- und Ausstiegspunkt war das ursprüngliche Konzept der strukturierten Programmierung im Vergleich zur schrittweisen Spaghetti-Codierung. Es besteht die Überzeugung, dass mehrere Exit-Point-Funktionen mehr Code erfordern, da Sie die für Variablen zugewiesenen Speicherplätze ordnungsgemäß bereinigen müssen. Stellen Sie sich ein Szenario vor, in dem die Funktion Variablen (Ressourcen) zuweist und ein frühzeitiges Verlassen der Funktion ohne ordnungsgemäße Bereinigung zu Ressourcenlecks führen würde. Darüber hinaus würde das Erstellen einer Bereinigung vor jedem Exit viel redundanten Code erzeugen.

PSS
quelle
Das ist wirklich kein Problem mit RAII
BigSandwich
14

Bei fast allem kommt es auf die Bedürfnisse des Ergebnisses an. In "alten Zeiten" führte Spaghetti-Code mit mehreren Rückgabepunkten zu Speicherlecks, da Codierer, die diese Methode bevorzugten, normalerweise nicht gut bereinigten. Es gab auch Probleme mit einigen Compilern, die den Verweis auf die Rückgabevariable "verloren" haben, da der Stapel während der Rückgabe gelöscht wurde, wenn von einem verschachtelten Bereich zurückgekehrt wurde. Das allgemeinere Problem war der wiedereintretende Code, bei dem versucht wird, dass der aufrufende Status einer Funktion genau dem Rückgabestatus entspricht. Mutatoren von oop haben dies verletzt und das Konzept wurde zurückgestellt.

Es gibt Ergebnisse, insbesondere Kernel, die die Geschwindigkeit benötigen, die mehrere Austrittspunkte bieten. Diese Umgebungen verfügen normalerweise über ein eigenes Speicher- und Prozessmanagement, sodass das Risiko eines Lecks minimiert wird.

Persönlich möchte ich einen einzigen Austrittspunkt haben, da ich ihn häufig verwende, um einen Haltepunkt in die return-Anweisung einzufügen und eine Codeüberprüfung durchzuführen, wie der Code diese Lösung bestimmt hat. Ich könnte einfach zum Eingang gehen und durchgehen, was ich mit weitgehend verschachtelten und rekursiven Lösungen mache. Als Code-Reviewer erfordern mehrere Rückgaben in einer Funktion eine viel tiefere Analyse. Wenn Sie dies also tun, um die Implementierung zu beschleunigen, rauben Sie Peter aus, um Paul zu retten. Bei Codeüberprüfungen wird mehr Zeit benötigt, was die Annahme einer effizienten Implementierung ungültig macht.

- 2 Cent

Weitere Informationen finden Sie in diesem Dokument: NISTIR 5459

sscheider
quelle
8
multiple returns in a function requires a much deeper analysisnur wenn die Funktion bereits riesig ist (> 1 Bildschirm), sonst erleichtert es die Analyse
dss539
1
Mehrfachrenditen machen die Analyse nie einfacher, nur das Gegenteil
GuidoG
1
Die Verbindung ist tot (404).
Fusi
1
@fusi - fand es auf archive.org und aktualisierte den Link hier
sscheider
4

Meiner Ansicht nach ist der Rat, eine Funktion (oder eine andere Kontrollstruktur) nur an einem Punkt zu verlassen, häufig überverkauft. Zwei Gründe werden normalerweise angegeben, um nur an einem Punkt zu beenden:

  1. Single-Exit-Code ist angeblich einfacher zu lesen und zu debuggen. (Ich gebe zu, dass ich nicht viel über diesen Grund nachdenke, aber er ist gegeben. Was wesentlich einfacher zu lesen und zu debuggen ist, ist ein Code mit einem Eintrag .)
  2. Single-Exit-Code verknüpft und kehrt sauberer zurück.

Der zweite Grund ist subtil und hat einige Vorteile, insbesondere wenn die Funktion eine große Datenstruktur zurückgibt. Ich würde mir jedoch keine allzu großen Sorgen machen, außer ...

Wenn Sie ein Schüler sind, möchten Sie in Ihrer Klasse Bestnoten erzielen. Mach was der Instruktor bevorzugt. Er hat wahrscheinlich einen guten Grund aus seiner Sicht; Zumindest lernst du seine Perspektive. Das hat Wert an sich.

Viel Glück.

thb
quelle
4

Früher war ich ein Verfechter des Single-Exit-Stils. Meine Argumentation kam hauptsächlich von Schmerzen ...

Single-Exit ist einfacher zu debuggen.

Angesichts der Techniken und Werkzeuge, über die wir heute verfügen, ist dies eine weitaus weniger vernünftige Position, da Unit-Tests und Protokollierung einen Single-Exit unnötig machen können. Das heißt, wenn Sie die Ausführung von Code in einem Debugger beobachten müssen, war es viel schwieriger, Code mit mehreren Exit-Punkten zu verstehen und damit zu arbeiten.

Dies traf insbesondere dann zu, wenn Sie Zuweisungen einwerfen mussten, um den Status zu überprüfen (in modernen Debuggern durch Überwachungsausdrücke ersetzt). Es war auch zu einfach, den Kontrollfluss so zu ändern, dass das Problem verborgen oder die Ausführung insgesamt unterbrochen wurde.

Single-Exit-Methoden waren im Debugger einfacher zu durchlaufen und leichter auseinander zu ziehen, ohne die Logik zu brechen.

Michael Murphree
quelle
0

Die Antwort ist sehr kontextabhängig. Wenn Sie eine grafische Benutzeroberfläche erstellen und eine Funktion haben, die APIs initialisiert und Fenster zu Beginn Ihres Hauptmenüs öffnet, ist sie voll von Aufrufen, die Fehler auslösen können, von denen jeder dazu führen würde, dass die Instanz des Programms geschlossen wird. Wenn Sie verschachtelte IF-Anweisungen verwenden und einrücken, kann Ihr Code schnell nach rechts verschoben werden. Die Rückkehr zu einem Fehler in jeder Phase ist möglicherweise besser und tatsächlich besser lesbar, während das Debuggen mit ein paar Flags im Code genauso einfach ist.

Wenn Sie jedoch unterschiedliche Bedingungen testen und je nach den Ergebnissen Ihrer Methode unterschiedliche Werte zurückgeben, ist es möglicherweise viel besser, einen einzigen Austrittspunkt zu haben. Ich habe in MATLAB an Bildverarbeitungsskripten gearbeitet, die sehr groß werden konnten. Mehrere Austrittspunkte können es äußerst schwierig machen, dem Code zu folgen. Switch-Anweisungen waren viel angemessener.

Das Beste, was Sie tun können, ist zu lernen, während Sie gehen. Wenn Sie Code für etwas schreiben, versuchen Sie, den Code anderer Leute zu finden und zu sehen, wie sie ihn implementieren. Entscheide, welche Bits du magst und welche nicht.

Flash_Steel
quelle
-5

Wenn Sie das Gefühl haben, mehrere Austrittspunkte in einer Funktion zu benötigen, ist die Funktion zu groß und macht zu viel.

Ich würde empfehlen, das Kapitel über Funktionen in Robert C. Martins Buch Clean Code zu lesen.

Im Wesentlichen sollten Sie versuchen, Funktionen mit maximal 4 Codezeilen zu schreiben.

Einige Notizen aus Mike Longs Blog :

  • Die erste Regel der Funktionen: Sie sollten klein sein
  • Die zweite Funktionsregel: Sie sollten kleiner sein
  • Blöcke in if-Anweisungen, while-Anweisungen, for-Schleifen usw. sollten eine Zeile lang sein
  • … Und diese Codezeile ist normalerweise ein Funktionsaufruf
  • Es sollte nicht mehr als eine oder vielleicht zwei Einrückungsstufen geben
  • Funktionen sollten eines tun
  • Funktionsanweisungen sollten sich alle auf derselben Abstraktionsebene befinden
  • Eine Funktion sollte nicht mehr als 3 Argumente haben
  • Ausgabeargumente sind ein Codegeruch
  • Das Übergeben eines Booleschen Flags an eine Funktion ist wirklich schrecklich. Sie machen per Definition zwei Dinge in der Funktion.
  • Nebenwirkungen sind Lügen.
Roger Dahl
quelle
29
4 Zeilen? Was codieren Sie, das Ihnen diese Einfachheit ermöglicht? Ich bezweifle wirklich, dass der Linux-Kernel oder Git das zum Beispiel tun.
Shinzou
7
"Ein boolesches Flag an eine Funktion zu übergeben ist wirklich schrecklich. Sie machen per Definition zwei Dinge in der Funktion." Per Definition? Nein ... dieser Boolesche Wert kann möglicherweise nur eine Ihrer vier Zeilen betreffen. Auch wenn ich damit einverstanden bin, die Funktionsgröße klein zu halten, sind vier etwas zu restriktiv. Dies sollte als sehr lose Richtlinie verstanden werden.
Jesse
12
Das Hinzufügen solcher Einschränkungen führt zwangsläufig zu verwirrendem Code. Es geht mehr darum, dass Methoden sauber und präzise sind und nur das tun, was sie sollen, ohne unnötige Nebenwirkungen.
Jesse
10
Eine der seltenen Antworten auf SO, die ich mir wünschte, ich könnte mehrmals abstimmen.
Steven Rands
8
Leider macht diese Antwort mehrere Dinge und musste wahrscheinlich in mehrere andere aufgeteilt werden - alle weniger als vier Zeilen.
Eli