Diese Frage ist eigentlich das Ergebnis einer interessanten Diskussion unter programmieren.reddit.com vor einiger Zeit. Es läuft im Grunde auf den folgenden Code hinaus:
int foo(int bar)
{
int return_value = 0;
if (!do_something( bar )) {
goto error_1;
}
if (!init_stuff( bar )) {
goto error_2;
}
if (!prepare_stuff( bar )) {
goto error_3;
}
return_value = do_the_thing( bar );
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
Die Verwendung von goto
hier scheint der beste Weg zu sein, was zu dem saubersten und effizientesten Code aller Möglichkeiten führt, oder zumindest scheint es mir so. Zitieren von Steve McConnell in Code Complete :
Das goto ist in einer Routine nützlich, die Ressourcen zuweist, Operationen an diesen Ressourcen ausführt und dann die Ressourcen freigibt. Mit einem goto können Sie in einem Abschnitt des Codes bereinigen. Das Goto verringert die Wahrscheinlichkeit, dass Sie vergessen, die Ressourcen an jedem Ort freizugeben, an dem Sie einen Fehler feststellen.
Eine weitere Unterstützung für diesen Ansatz stammt aus dem Linux-Gerätetreiberbuch in diesem Abschnitt .
Was denken Sie? Ist dieser Fall eine gültige Verwendung für goto
in C? Würden Sie andere Methoden bevorzugen, die komplizierteren und / oder weniger effizienten Code erzeugen, aber vermeiden goto
?
quelle
on error goto error
Fehlerbehandlungssystem :)Antworten:
FWIF, ich finde die Fehlerbehandlungssprache, die Sie im Beispiel der Frage angegeben haben, lesbarer und verständlicher als jede der bisher in den Antworten angegebenen Alternativen. Obwohl dies
goto
im Allgemeinen eine schlechte Idee ist, kann es für die Fehlerbehandlung nützlich sein, wenn es auf einfache und einheitliche Weise durchgeführt wird. In dieser Situation wird es, obwohl es sich um eine handeltgoto
, klar definiert und mehr oder weniger strukturiert verwendet.quelle
In der Regel ist es eine gute Idee, goto zu vermeiden, aber die Missbräuche, die vorherrschten, als Dijkstra zum ersten Mal "GOTO als schädlich" schrieb, kommen den meisten Menschen heutzutage nicht einmal in den Sinn.
Was Sie skizzieren, ist eine verallgemeinerbare Lösung für das Problem der Fehlerbehandlung - es ist in Ordnung für mich, solange es sorgfältig verwendet wird.
Ihr spezielles Beispiel kann wie folgt vereinfacht werden (Schritt 1):
Fortsetzung des Prozesses:
Dies entspricht meines Erachtens dem ursprünglichen Code. Dies sieht besonders sauber aus, da der ursprüngliche Code selbst sehr sauber und gut organisiert war. Oft sind die Codefragmente nicht so ordentlich (obwohl ich ein Argument akzeptieren würde, dass sie sein sollten); Beispielsweise muss häufig mehr Status an die Initialisierungsroutinen (Setup-Routinen) übergeben werden als gezeigt, und daher muss auch mehr Status an die Bereinigungsroutinen übergeben werden.
quelle
Ich bin überrascht, dass niemand diese Alternative vorgeschlagen hat. Obwohl die Frage schon eine Weile besteht, füge ich sie hinzu: Eine gute Möglichkeit, dieses Problem zu beheben, besteht darin, Variablen zu verwenden, um den aktuellen Status zu verfolgen. Dies ist eine Technik, die verwendet werden kann, unabhängig davon, ob
goto
zum Auffinden des Bereinigungscodes verwendet wird oder nicht . Wie jede Codierungstechnik hat sie Vor- und Nachteile und ist nicht für jede Situation geeignet. Wenn Sie sich jedoch für einen Stil entscheiden, sollten Sie darüber nachdenken - insbesondere, wenn Sie vermeiden möchten,goto
ohne tief verschachtelteif
s zu erhalten.Die Grundidee ist, dass es für jede Bereinigungsaktion, die möglicherweise ausgeführt werden muss, eine Variable gibt, anhand deren Wert wir erkennen können, ob die Bereinigung durchgeführt werden muss oder nicht.
Ich werde zuerst die
goto
Version zeigen , da sie näher am Code in der ursprünglichen Frage liegt.Ein Vorteil gegenüber einigen anderen Techniken besteht darin, dass, wenn die Reihenfolge der Initialisierungsfunktionen geändert wird, die korrekte Bereinigung weiterhin erfolgt - beispielsweise unter Verwendung der
switch
in einer anderen Antwort beschriebenen Methode, wenn sich die Reihenfolge der Initialisierung ändert, dann dieswitch
muss sehr sorgfältig bearbeitet werden, um zu vermeiden, dass versucht wird, etwas zu bereinigen, das überhaupt nicht initialisiert wurde.Einige mögen nun argumentieren, dass diese Methode eine ganze Reihe zusätzlicher Variablen hinzufügt - und in diesem Fall ist dies tatsächlich der Fall -, aber in der Praxis verfolgt eine vorhandene Variable häufig bereits den erforderlichen Zustand oder kann dazu gebracht werden, diesen zu verfolgen. Wenn dies beispielsweise
prepare_stuff()
tatsächlich ein Aufruf vonmalloc()
oder an istopen()
, kann die Variable verwendet werden, die den zurückgegebenen Zeiger oder Dateideskriptor enthält - zum Beispiel:Wenn wir nun zusätzlich den Fehlerstatus mit einer Variablen verfolgen, können wir dies
goto
vollständig vermeiden und trotzdem korrekt bereinigen, ohne dass Einrückungen vorhanden sind, die immer tiefer werden, je mehr Initialisierung wir benötigen:Auch hier gibt es mögliche Kritikpunkte:
if (oksofar)
Fehlerfall optimieren die meisten Compiler die Reihenfolge der fehlgeschlagenen Überprüfungen auf einen einzigen Sprung zum Bereinigungscode (GCC sicherlich) - und in jedem Fall ist der Fehlerfall normalerweise weniger kritisch für die Leistung.Fügt dies nicht noch eine weitere Variable hinzu? In diesem Fall ja, aber oft kann die
return_value
Variable verwendet werden, um die Rolle zu spielen, dieoksofar
hier spielt. Wenn Sie Ihre Funktionen so strukturieren, dass Fehler konsistent zurückgegeben werden, können Sie sogar die zweiteif
in jedem Fall vermeiden :Einer der Vorteile einer solchen Codierung besteht darin, dass die Konsistenz bedeutet, dass jeder Ort, an dem der ursprüngliche Programmierer vergessen hat, den Rückgabewert zu überprüfen, wie ein schmerzender Daumen hervorsteht, was das Auffinden (dieser einen Klasse von) Fehlern erheblich erleichtert.
Also - dies ist (noch) ein weiterer Stil, mit dem dieses Problem gelöst werden kann. Bei richtiger Anwendung ermöglicht es sehr sauberen, konsistenten Code - und wie bei jeder Technik kann es in den falschen Händen dazu führen, dass Code erzeugt wird, der langwierig und verwirrend ist :-)
quelle
Das Problem mit dem
goto
Schlüsselwort wird meistens missverstanden. Es ist nicht einfach böse. Sie müssen sich nur der zusätzlichen Steuerpfade bewusst sein, die Sie mit jedem goto erstellen. Es wird schwierig, über Ihren Code und damit seine Gültigkeit nachzudenken.FWIW, wenn Sie die Tutorials zu developer.apple.com nachschlagen, gehen sie bei der Fehlerbehandlung auf die gleiche Weise vor.
Wir verwenden keine gotos. Eine höhere Bedeutung wird den Rückgabewerten beigemessen. Die Ausnahmebehandlung erfolgt über
setjmp/longjmp
- was auch immer Sie können.quelle
An der goto-Aussage ist moralisch nichts mehr falsch als an (nichtigen) * Zeigern etwas moralisch Falsches.
Alles hängt davon ab, wie Sie das Tool verwenden. In dem von Ihnen vorgestellten (trivialen) Fall kann eine case-Anweisung dieselbe Logik erzielen, allerdings mit mehr Aufwand. Die eigentliche Frage ist: "Was ist meine Geschwindigkeitsanforderung?"
goto ist einfach nur schnell, besonders wenn Sie darauf achten, dass es zu einem kurzen Sprung kompiliert wird. Perfekt für Anwendungen, bei denen Geschwindigkeit an erster Stelle steht. Für andere Anwendungen ist es wahrscheinlich sinnvoll, den Overhead-Treffer mit if / else + case zu nehmen, um die Wartbarkeit zu gewährleisten.
Denken Sie daran: goto tötet keine Anwendungen, Entwickler töten Anwendungen.
UPDATE: Hier ist das Fallbeispiel
quelle
GOTO ist nützlich. Dies kann Ihr Prozessor und deshalb sollten Sie Zugriff darauf haben.
Manchmal möchten Sie Ihrer Funktion etwas hinzufügen, und mit einem einzigen Schritt können Sie dies ganz einfach tun. Es kann Zeit sparen ..
quelle
Im Allgemeinen würde ich die Tatsache, dass ein Teil des Codes am klarsten geschrieben werden könnte,
goto
als Symptom dafür betrachten, dass der Programmablauf wahrscheinlich komplizierter ist als allgemein wünschenswert. Die Kombination anderer Programmstrukturen auf seltsame Weise, um die Verwendung von zu vermeiden,goto
würde versuchen, das Symptom und nicht die Krankheit zu behandeln. Ihr spezielles Beispiel ist möglicherweise nicht allzu schwierig zu implementieren, ohnegoto
:Wenn die Bereinigung jedoch nur stattfinden sollte, wenn die Funktion fehlschlug, konnte der
goto
Fall behandelt werden, indem einreturn
kurz vor dem ersten Zieletikett gesetzt wurde. Der obige Code würde das Hinzufügen einesreturn
in der mit gekennzeichneten Zeile erfordern*****
.Im Szenario "Bereinigen auch im Normalfall" würde ich die Verwendung
goto
als klarer als diedo
/while(0)
-Konstrukte betrachten, unter anderem, weil die Zielbezeichnungen selbst praktisch "LOOK AT ME" schreien, weitaus mehr als diebreak
unddo
/ -Konstruktewhile(0)
. Für den Fall "Nur bei Fehler bereinigen" muss sich diereturn
Anweisung unter dem Gesichtspunkt der Lesbarkeit an der schlechtesten Stelle befinden (Rückgabeanweisungen sollten im Allgemeinen entweder am Anfang einer Funktion stehen oder wie "aussieht"). das Ende); Einreturn
kurz vor einem Zieletikett zu haben, erfüllt diese Qualifikation viel leichter als ein kurz vor dem Ende einer "Schleife".Übrigens, ein Szenario, das ich manchmal
goto
zur Fehlerbehandlung verwende, befindet sich in einerswitch
Anweisung, wenn der Code für mehrere Fälle denselben Fehlercode aufweist. Obwohl mein Compiler oft klug genug ist, um zu erkennen, dass mehrere Fälle mit demselben Code enden, ist es meiner Meinung nach klarer zu sagen:Obwohl man die
goto
Anweisungen durch ersetzen könnte{handle_error(); break;}
und obwohl man einedo
/while(0)
Schleife zusammen mit verwenden könntecontinue
, um das umschlossene Paket mit bedingter Ausführung zu verarbeiten, denke ich nicht, dass dies klarer ist als die Verwendung von agoto
. Ferner kann , während es den Code abschreiben könnte möglich sein , vonPACKET_ERROR
überallgoto PACKET_ERROR
verwendet wird, und während ein Compiler einmal , um den duplizierten Code schreiben könnte und die meisten Vorkommen mit einem Sprung auf die freigegebenen Kopie ersetzen, die mit dergoto
es einfacher zu bemerken Orten macht die das Paket etwas anders einrichten (z. B. wenn der Befehl "Bedingt ausführen" beschließt, nicht auszuführen).quelle
Ich persönlich bin ein Anhänger der "The Power of Ten - 10 Regeln für das Schreiben von sicherheitskritischem Code" .
Ich werde einen kleinen Ausschnitt aus diesem Text hinzufügen, der zeigt, was ich für eine gute Idee zu goto halte.
Regel: Beschränken Sie den gesamten Code auf sehr einfache Kontrollflusskonstrukte. Verwenden Sie keine goto-Anweisungen, setjmp- oder longjmp-Konstrukte und keine direkte oder indirekte Rekursion.
Begründung: Ein einfacherer Kontrollfluss führt zu stärkeren Überprüfungsmöglichkeiten und führt häufig zu einer verbesserten Codeklarheit. Die Verbannung der Rekursion ist hier vielleicht die größte Überraschung. Ohne Rekursion haben wir jedoch garantiert einen azyklischen Funktionsaufrufgraphen, der von Codeanalysatoren ausgenutzt werden kann und direkt dazu beitragen kann, zu beweisen, dass alle Ausführungen, die begrenzt werden sollten, tatsächlich begrenzt sind. (Beachten Sie, dass diese Regel nicht erfordert, dass alle Funktionen einen einzigen Rückgabepunkt haben - obwohl dies häufig auch den Kontrollfluss vereinfacht. Es gibt jedoch genügend Fälle, in denen eine frühzeitige Fehlerrückgabe die einfachere Lösung ist.)
Die Verwendung von goto zu verbannen scheint schlecht, aber:
Wenn die Regeln auf den ersten Blick drakonisch erscheinen, denken Sie daran, dass sie es ermöglichen sollen, Code zu überprüfen, bei dem Ihr Leben buchstäblich von seiner Richtigkeit abhängt: Code, der zur Steuerung des Flugzeugs verwendet wird, mit dem Sie fliegen, des Kernkraftwerks ein paar Meilen von Ihrem Wohnort entfernt oder das Raumschiff, das Astronauten in die Umlaufbahn bringt. Die Regeln wirken wie der Sicherheitsgurt in Ihrem Auto: Anfangs sind sie vielleicht etwas unangenehm, aber nach einer Weile wird ihre Verwendung zur Selbstverständlichkeit und ihre Nichtbenutzung wird unvorstellbar.
quelle
goto
besteht, eine Reihe von „cleveren“ Booleschen Werten in tief verschachtelten ifs oder Schleifen zu verwenden. Das hilft wirklich nicht. Vielleicht werden Ihre Werkzeuge es besser machen, aber Sie werden es nicht und Sie sind wichtiger.Ich bin damit einverstanden, dass die in der Frage angegebene umgekehrte Reihenfolge in den meisten Funktionen die sauberste Methode zum Aufräumen ist. Aber ich wollte auch darauf hinweisen, dass Sie manchmal möchten, dass Ihre Funktion trotzdem aufgeräumt wird. In diesen Fällen verwende ich die folgende Variante, wenn if (0) {label:} idiom, um zum richtigen Punkt des Bereinigungsprozesses zu gelangen:
quelle
Mir scheint, das
cleanup_3
sollte seine Bereinigung machen, dann anrufencleanup_2
. In ähnlicher Weisecleanup_2
sollte es bereinigen und dann cleanup_1 aufrufen. Es scheint , dass , wann immer Sie tuncleanup_[n]
, diecleanup_[n-1]
erforderlich ist, so sollte es in der Verantwortung des Verfahrens sein (so dass zum Beispielcleanup_3
kann nie ohne Aufruf aufgerufen werdencleanup_2
und möglicherweise verursacht ein Leck.)Bei diesem Ansatz würden Sie anstelle von gotos einfach die Bereinigungsroutine aufrufen und dann zurückkehren.
Der
goto
Ansatz ist jedoch nicht falsch oder schlecht , es ist nur erwähnenswert, dass es nicht unbedingt der "sauberste" Ansatz (IMHO) ist.Wenn Sie nach der optimalen Leistung suchen, ist die
goto
Lösung wahrscheinlich die beste. Ich erwarte jedoch nur, dass es in einigen wenigen leistungskritischen Anwendungen (z. B. Gerätetreibern, eingebetteten Geräten usw.) relevant ist. Andernfalls handelt es sich um eine Mikrooptimierung, die eine niedrigere Priorität als die Klarheit des Codes hat.quelle
Ich denke, dass die Frage hier in Bezug auf den gegebenen Code trügerisch ist.
Erwägen:
Deshalb: do_something (), init_stuff () und prepare_stuff () sollten ihre eigene Bereinigung durchführen . Eine separate Funktion cleanup_1 (), die nach do_something () bereinigt wird, verstößt gegen die Philosophie der Kapselung. Es ist schlechtes Design.
Wenn sie selbst bereinigt haben, wird foo () ganz einfach.
Andererseits. Wenn foo () tatsächlich einen eigenen Status erstellt hätte, der abgerissen werden müsste, wäre goto angemessen.
quelle
Folgendes habe ich bevorzugt:
quelle
Alte Diskussion jedoch ... was ist mit der Verwendung von "Pfeil-Anti-Muster" und dem späteren Einkapseln jeder verschachtelten Ebene in eine statische Inline-Funktion? Der Code sieht sauber aus, ist optimal (wenn Optimierungen aktiviert sind) und es wird kein goto verwendet. Kurz gesagt, teilen und erobern. Unten ein Beispiel:
In Bezug auf den Speicherplatz erstellen wir die dreifache Variable im Stapel, was nicht gut ist. Dies verschwindet jedoch, wenn beim Kompilieren mit -O2 die Variable aus dem Stapel entfernt und in diesem einfachen Beispiel ein Register verwendet wird. Was ich aus dem obigen Block bekommen habe,
gcc -S -O2 test.c
war das Folgende:quelle
Ich bevorzuge die im folgenden Beispiel beschriebene Technik ...
}}
Quelle: http://blog.staila.com/?p=114
quelle
Wir verwenden die
Daynix CSteps
Bibliothek als weitere Lösung für das " goto-Problem " in init-Funktionen.Sehen Sie hier und hier .
quelle
goto