Ist dies ein anständiger Anwendungsfall für goto in C?

59

Ich zögere wirklich, dies zu fragen, weil ich nicht "Debatten, Argumente, Umfragen oder erweiterte Diskussionen anregen" möchte, aber ich bin neu in C und möchte mehr Einsicht in die in der Sprache verwendeten Muster gewinnen.

Ich habe kürzlich eine gewisse Abneigung gegen den gotoBefehl gehört, aber ich habe auch kürzlich einen anständigen Anwendungsfall dafür gefunden.

Code wie folgt:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

Wenn der Aufräum-Teil alles sehr ähnlich ist, könnte etwas hübscher (meiner Meinung nach?) Geschrieben werden:

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

Ist dies ein gängiger oder akzeptabler Anwendungsfall von gotoin C? Gibt es einen anderen / besseren Weg, dies zu tun?

Robz
quelle
3
Siehe auch diese Frage (und meine Antwort ).
Keith Thompson
2
Der Compiler ist mehr als nur eine Zuckerbeschichtung, er garantiert eine Bereinigung und eine Fehlerprüfung, während Sie in C manuell vorgehen müssen, um DRY zu verletzen und zu gewährleisten, dass Sie an einem Ort zu faul waren, um den Fehlercode zu überprüfen, oder zum falschen Etikett oder dergleichen übergegangen sind.
DeadMG
3
@Izkata: Nein, es geht darum, Funktionen (wie Destruktoren) zu haben, die implizit vom Compiler aufgerufen werden, wenn eine Ausnahme ausgelöst wird.
DeadMG
6
@DeadMG: Die Verwendung einer anderen Sprache ist nicht immer eine Option.
Benjamin Kloster
13
Ich denke bei entsprechender Labelbenennung sieht das Beispiel vielleicht ganz anständig aus:goto hell;
gnat

Antworten:

48

Die gotoAnweisung (und seine entsprechenden Etiketten) sind ein Stromregel primitiver (zusammen mit bedingter Ausführung einer Anweisung). Damit meine ich, dass sie Ihnen den Aufbau von Netzwerken zur Programmflusssteuerung ermöglichen. Sie können sich diese als Modellierung der Pfeile zwischen den Knoten eines Flussdiagramms vorstellen.

Einige davon können sofort optimiert werden, wenn ein direkter linearer Fluss vorliegt (Sie verwenden nur eine Folge grundlegender Anweisungen). Andere Muster werden am besten durch strukturierte Programmierkonstrukte ersetzt, sofern diese verfügbar sind. Wenn es wie eine whileSchleife aussieht , verwenden Sie eine whileSchleife , OK? Die strukturierten Programmiermuster sind definitiv zumindest potenziell eindeutiger als ein Durcheinander von gotoAussagen.

C enthält jedoch nicht alle möglichen strukturierten Programmierkonstrukte. (Es ist mir nicht klar, dass alle relevanten bereits entdeckt wurden. Die Entdeckungsrate ist jetzt langsam, aber ich würde zögern, zu sagen, dass alle gefunden wurden.) Von denen, die wir kennen, fehlt C definitiv die try/ catch/ finallyStruktur (und Ausnahmen auch). Es fehlt auch Multi-Level- breakFrom-Loop. Dies sind die Arten von Dingen, mit denen gotoman etwas implementieren kann. Es ist möglich, auch andere Schemata zu verwenden - wir wissen, dass C eine ausreichende Menge vongotoPrimitive - aber diese beinhalten oft das Erstellen von Flag-Variablen und viel komplexeren Schleifen- oder Guard-Bedingungen; Durch eine stärkere Verknüpfung der Kontrollanalyse mit der Datenanalyse wird das Programm insgesamt schwerer verständlich. Dies erschwert auch die Optimierung des Compilers und die schnelle Ausführung der CPU (die meisten Flusssteuerungskonstrukte sind - und definitiv goto - sehr billig).

Daher sollten Sie nicht verwenden, es gotosei denn, Sie sollten sich bewusst sein, dass es vorhanden ist und dass es möglicherweise benötigt wird, und wenn Sie es benötigen, sollten Sie sich nicht zu schlecht fühlen. Ein Beispiel für einen Fall, in dem dies erforderlich ist, ist die Freigabe der Ressource, wenn eine aufgerufene Funktion eine Fehlerbedingung zurückgibt. (Das heißt, try/ finally.) Es ist möglich, das zu schreiben, ohne gotoes zu tun, aber das kann Nachteile für sich haben, wie zum Beispiel die Probleme, es zu warten. Ein Beispiel für den Fall:

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

Der Code könnte noch kürzer sein, aber es reicht aus, um den Punkt zu demonstrieren.

Donal Fellows
quelle
4
+1: In C wird goto technisch nie "benötigt" - es gibt immer einen Weg, es wird chaotisch ..... Für eine robuste Reihe von Richtlinien für die Verwendung von goto schauen Sie sich MISRA C an.
mattnz
1
Würden Sie es vorziehen try/catch/finally, gototrotz der ähnlichen und dennoch weit verbreiteten Form von Spaghetti-Code (da dieser sich über mehrere Funktionen / Module erstrecken kann), die mit Spaghetti-Code möglich ist try/catch/finally?
autistische
65

Ja.

Es wird zum Beispiel im Linux-Kernel verwendet. Hier ist eine E-Mail vom Ende eines Threads von vor fast einem Jahrzehnt , der meinen aufdringlich macht:

Von: Robert Love
Betreff: Re: irgendeine Chance auf 2.6.0-Test *?
Datum: 12. Januar 2003 17:58:06 -0500

Am Sonntag, den 12.01.2003 um 17:22 schrieb Rob Wilkens:

Ich sage "Bitte benutze nicht goto" und habe stattdessen eine "cleanup_lock" -Funktion und füge diese vor allen return-Anweisungen hinzu. Es sollte keine Belastung sein. Ja, es fordert den Entwickler auf, etwas härter zu arbeiten, aber das Endergebnis ist besserer Code.

Nein, es ist ekelhaft und bläht den Kernel auf . Es enthält eine Menge Junk für N Fehlerpfade, anstatt den Exit-Code einmal am Ende zu haben. Der Cache-Fußabdruck ist der Schlüssel und Sie haben ihn gerade getötet.

Es ist auch nicht einfacher zu lesen.

Als letztes Argument lässt es uns nicht sauber das übliche stapelartige Auf- und Abwickeln machen , d. H

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

Jetzt hör auf damit.

Robert Love

Das heißt, es erfordert viel Disziplin, sich von der Erstellung von Spaghetti-Code abzuhalten, sobald Sie sich an die Verwendung von goto gewöhnt haben. Wenn Sie also nicht etwas schreiben, das Geschwindigkeit und geringen Speicherbedarf erfordert (wie einen Kernel oder ein eingebettetes System), sollten Sie das wirklich tun Denken Sie darüber nach, bevor Sie das erste Goto schreiben.

Izkata
quelle
21
Beachten Sie, dass sich ein Kernel von einem Nicht-Kernel-Programm in Bezug auf die Priorität von Raw-Geschwindigkeit und Lesbarkeit unterscheidet. Mit anderen Worten, sie haben BEREITS ein Profil erstellt und festgestellt, dass sie ihren Code für die Geschwindigkeit mit goto optimieren müssen.
11
Verwenden Sie die Stapelabwicklung, um Fehler zu beseitigen, ohne auf den Stapel zu drücken! Dies ist eine großartige Verwendung von goto.
mike30
1
@ user1249, Quatsch, Sie können nicht jede {vergangene, existierende, zukünftige} App profilieren, die einen Teil {Bibliothek, Kernel} -Code verwendet. Sie müssen einfach schnell sein.
Pacerier
1
Unabhängig: Ich bin erstaunt, wie Leute Mailinglisten verwenden können, um etwas zu erledigen, geschweige denn solche massiven Projekte. Es ist nur so ... primitiv. Wie kommen die Leute mit dem Feuerwehrhaus der Nachrichten herein ?!
Alexander
2
Ich bin nicht so besorgt über die Moderation. Wenn jemand weich genug ist, um sich von einem Arschloch im Internet abweisen zu lassen, ist Ihr Projekt ohne ihn wahrscheinlich besser dran. Ich bin mehr besorgt über die Unpraktikabilität, mit dem Sperren eingehender Nachrichten Schritt zu halten, und darüber, wie Sie möglicherweise eine natürliche Hintergrunddiskussion mit so wenig Werkzeug haben könnten, um beispielsweise Angebote im Auge zu behalten.
Alexander
14

Meiner Meinung nach ist der von Ihnen veröffentlichte Code ein Beispiel für eine gültige Verwendung von goto, da Sie nur nach unten springen und ihn nur wie einen primitiven Ausnahmebehandler verwenden.

Aufgrund der alten goto-Debatte meiden Programmierer jedoch seit gotorund 40 Jahren und sind daher nicht daran gewöhnt, Code mit goto zu lesen. Dies ist ein triftiger Grund, um Goto zu vermeiden: Es ist einfach nicht der Standard.

Ich hätte den Code so umgeschrieben, dass er von C-Programmierern leichter gelesen werden kann:

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

Vorteile dieses Designs:

  • Die Funktion, die die eigentliche Arbeit ausführt, muss sich nicht mit Aufgaben befassen, die für ihren Algorithmus irrelevant sind, wie z. B. die Zuweisung von Daten.
  • C-Programmierern wird der Code weniger fremd erscheinen, da sie Angst vor goto und labels haben.
  • Sie können die Fehlerbehandlung und die Freigabe an derselben Stelle außerhalb der Funktion, die den Algorithmus ausführt, zentralisieren. Es macht keinen Sinn, dass eine Funktion mit ihren eigenen Ergebnissen umgeht.
Doc Brown
quelle
11

Ein berühmtes Papier, das den Fall einer gültigen Verwendung von beschreibt, war Structured Programming with GOTO Statement von Donald E. Knuth (Stanford University). Das Papier erschien in den Tagen, in denen die Verwendung von GOTO von einigen als Sünde angesehen wurde und als die Bewegung für strukturierte Programmierung ihren Höhepunkt erreichte. Vielleicht möchten Sie einen Blick auf GoTo als schädlich werfen .

Keine Chance
quelle
9

In Java würden Sie das so machen:

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

Ich benutze das viel. So sehr ich es auch nicht mag goto, in den meisten anderen Sprachen im C-Stil verwende ich Ihren Code. Es gibt keinen anderen guten Weg, dies zu tun. (Das Herausspringen aus verschachtelten Schleifen ist ein ähnlicher Fall. In Java verwende ich eine beschriftete breakund in allen anderen Fällen eine goto.)

RalphChapin
quelle
3
Oh, das ist eine ordentliche Kontrollstruktur.
Bryan Boettcher
4
Das ist wirklich interessant. Normalerweise würde ich versuchen, / catch / finally-Struktur in Java zu verwenden (Ausnahmen zu werfen, anstatt zu brechen).
Robz
5
Das ist wirklich unlesbar (zumindest für mich). Sofern vorhanden, sind Ausnahmen weitaus besser.
m3th0dman
1
@ m3th0dman In diesem Beispiel stimme ich Ihnen zu (Fehlerbehandlung). Es gibt jedoch auch andere (nicht außergewöhnliche) Fälle, in denen sich diese Redewendung als nützlich erweisen könnte.
Konrad Rudolph
1
Ausnahmen sind teuer, sie müssen einen Fehler, Stacktrace und viel mehr Junk generieren. Dieser Etikettenumbruch bietet einen sauberen Ausgang aus einer Prüfschleife. es sei denn, man kümmert sich nicht um Speicher und Geschwindigkeit, dann verwende ich für alles, was mir wichtig ist, eine Ausnahme.
Tschallacka
8

Ich denke, es ist ein anständiger Anwendungsfall, aber wenn "error" nichts anderes als ein boolescher Wert ist, gibt es einen anderen Weg, um das zu erreichen, was Sie wollen:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

Dies nutzt die Kurzschlussauswertung von Booleschen Operatoren. Wenn dies "besser" ist, hängt es von Ihrem persönlichen Geschmack ab und wie Sie an diese Redewendung gewöhnt sind.

Doc Brown
quelle
1
Das Problem dabei ist, dass errorder Wert mit der gesamten ODER-Verknüpfung bedeutungslos werden kann.
James
@ James: bearbeitete meine Antwort aufgrund Ihres Kommentars
Doc Brown
1
Das reicht nicht aus. Wenn bei der ersten Funktion ein Fehler aufgetreten ist, möchte ich die zweite oder dritte Funktion nicht ausführen.
Robz
2
Wenn mit Kurz Hand Sie Auswertung bedeuten Kurzschlussauswertung, dann ist dies genau das nicht , was hier durch die Verwendung von bitweise getan oder anstelle von logischen OR.
Chris sagt Reinstate Monica
1
@ ChristianRau: danke, meine Antwort entsprechend bearbeitet
Doc Brown
6

Der Linux Style Guide gibt spezifische Gründe für die Verwendung von goto's, die Ihrem Beispiel entsprechen:

https://www.kernel.org/doc/Documentation/process/coding-style.rst

Das Grundprinzip für die Verwendung von gotos ist:

  • Unbedingte Aussagen sind leichter zu verstehen und zu befolgen
  • Verschachtelung wird reduziert
  • fehler durch nicht aktualisierung einzelner ausgangspunkte bei änderungen werden verhindert
  • spart dem Compiler Arbeit, um redundanten Code weg zu optimieren;)

Haftungsausschluss Ich soll meine Arbeit nicht teilen. Die Beispiele hier sind ein bisschen erfunden, bitte tragen Sie mit mir.

Dies ist gut für die Speicherverwaltung. Ich habe kürzlich an Code gearbeitet, der dynamisch zugewiesenen Speicher hatte (zum Beispiel einen char *von einer Funktion zurückgegebenen). Eine Funktion, die einen Pfad untersucht und feststellt, ob der Pfad gültig ist, indem die Token des Pfads analysiert werden:

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

Für mich ist der folgende Code viel netter und einfacher zu pflegen, wenn Sie Folgendes hinzufügen müssen varNplus1:

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

Jetzt hatte der Code alle möglichen anderen Probleme, nämlich dass N irgendwo über 10 lag und die Funktion über 450 Zeilen mit 10 Verschachtelungsebenen an einigen Stellen.

Aber ich habe meinem Vorgesetzten angeboten, es umzugestalten, was ich auch getan habe, und jetzt gibt es eine Reihe von Funktionen, die alle kurz sind und alle den Linux-Stil haben

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

Wenn wir das Äquivalent ohne gotos betrachten:

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

Für mich ist es im ersten Fall offensichtlich, dass NULLwir hier raus und zurückkehren , wenn die erste Funktion zurückkehrt 0. Im zweiten Fall muss ich nach unten scrollen, um zu sehen, dass das if die gesamte Funktion enthält. Zugegeben, der erste zeigt mir dies stilistisch an (der Name " out") und der zweite macht es syntaktisch. Der erste ist noch offensichtlicher.

Außerdem bevorzuge ich es sehr, free()Anweisungen am Ende einer Funktion zu haben. Dies liegt meiner Erfahrung nach zum Teil daran, dass free()Aussagen in der Mitte von Funktionen schlecht riechen und mir signalisieren, dass ich ein Unterprogramm erstellen sollte. In diesem Fall habe ich var1in meiner Funktion erstellt und konnte es nicht free()in einem Unterprogramm, aber das ist der Grund goto out_free, warum der Goto-Out-Stil so praktisch ist.

Ich denke, Programmierer müssen in dem Glauben erzogen werden, dass gotoes böse ist. Wenn sie dann ausgereift sind, sollten sie den Linux-Quellcode durchsuchen und den Linux-Styleguide lesen.

Ich sollte hinzufügen, dass ich diesen Stil sehr konsequent verwende, jede Funktion hat ein int retval, ein out_freelabel und ein out label. Aufgrund der stilistischen Konsistenz wird die Lesbarkeit verbessert.

Bonus: Bricht und geht weiter

Angenommen, Sie haben eine while-Schleife

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

An diesem Code sind andere Dinge falsch, aber eine Sache ist die continue-Anweisung. Ich würde das Ganze gerne umschreiben, hatte aber den Auftrag, es ein wenig zu modifizieren. Ich hätte Tage gebraucht, um es auf eine Weise umzugestalten, die mich zufriedenstellte, aber die eigentliche Änderung war ungefähr ein halber Tag Arbeit. Das Problem ist, dass wir, auch wenn wir " continue" noch frei var1und brauchen var2. Ich musste ein hinzufügen var3, und es brachte mich dazu, kotzen zu müssen, um die free () -Anweisungen zu spiegeln.

Ich war zu der Zeit ein relativ neuer Praktikant, aber ich hatte vor einiger Zeit zum Spaß den Linux-Quellcode angesehen und meinen Vorgesetzten gefragt, ob ich eine goto-Anweisung verwenden könnte. Er sagte ja und ich tat dies:

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

Ich denke, die Fortsetzung ist im besten Fall in Ordnung, aber für mich sind sie wie ein Goto mit einem unsichtbaren Etikett. Das gleiche gilt für Pausen. Ich würde es immer noch vorziehen, fortzufahren oder abzubrechen, es sei denn, wie hier, es zwingt Sie, Änderungen an mehreren Stellen zu spiegeln.

Und ich sollte hinzufügen, dass diese Verwendung von goto next;und das next:Etikett für mich unbefriedigend sind. Sie sind lediglich besser als das Spiegeln free()der count++Aussagen und Aussagen.

gotosind fast immer falsch, aber man muss wissen, wann sie gut zu gebrauchen sind.

Eine Sache, die ich nicht besprochen habe, ist die Fehlerbehandlung, die in anderen Antworten behandelt wurde.

Performance

Man kann sich die Implementierung von strtok () ansehen: http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

Bitte korrigieren Sie mich, wenn ich falsch liege, aber ich glaube, dass das cont:Label und die goto cont;Aussage der Leistung dienen (sie machen den Code mit Sicherheit nicht besser lesbar). Sie könnten durch lesbaren Code ersetzt werden

while( isDelim(*s++,delim));

Begrenzer zu überspringen. Um jedoch so schnell wie möglich zu sein und unnötige Funktionsaufrufe zu vermeiden, tun sie dies auf diese Weise.

Ich habe die Zeitung von Dijkstra gelesen und finde sie ziemlich esoterisch.

google "dijkstra goto statement" als schädlich eingestuft, da ich nicht genug Reputation habe, um mehr als 2 Links zu posten.

Ich habe gesehen, dass es als ein Grund angeführt wurde, goto's nicht zu verwenden, und dass das Lesen nichts daran geändert hat, was meine Verwendung von goto's angeht.

Nachtrag :

Ich habe mir eine ordentliche Regel ausgedacht, während ich darüber nachdachte, wie es weitergeht und wie es weitergeht.

  • Wenn Sie in einer while-Schleife eine continue-Anweisung haben, sollte der Rumpf der while-Schleife eine Funktion und die continue-Anweisung eine return-Anweisung sein.
  • Wenn Sie in einer while-Schleife eine break-Anweisung haben, sollte die while-Schleife selbst eine Funktion und die break-Anweisung eine return-Anweisung sein.
  • Wenn Sie beide haben, ist möglicherweise etwas nicht in Ordnung.

Aufgrund von Umfangsproblemen ist dies nicht immer möglich, aber ich habe festgestellt, dass es dadurch viel einfacher ist, über meinen Code nachzudenken. Mir war aufgefallen, dass es mir immer ein schlechtes Gefühl gab, wenn eine while-Schleife eine Pause oder ein Fortsetzen hatte.

Philippe Carphin
quelle
2
+1, aber kann ich in einem Punkt nicht zustimmen? "Ich denke, Programmierer müssen in dem Glauben erzogen werden, dass Gotos böse sind." Vielleicht, aber ich lernte 1975 das Programmieren in BASIC mit Zeilennummern und GOTOs ohne Texteditor. Zehn Jahre später lernte ich das strukturierte Programmieren kennen. Danach brauchte ich einen Monat, um GOTO alleine ohne GOTO zu verwenden jeder Druck zu stoppen. Heute benutze ich GOTO aus verschiedenen Gründen ein paar Mal im Jahr, aber es kommt nicht viel auf. Nicht erzogen worden zu sein, um zu glauben, dass GOTO böse ist, hat mir keinen Schaden zugefügt, den ich kenne, und es könnte sogar etwas Gutes getan haben. Das bin nur ich.
25.
1
Ich denke du hast recht damit. Ich kam auf die Idee, dass GOTOs nicht verwendet werden sollten, und rein zufällig durchsuchte ich den Linux-Quellcode zu einer Zeit, als ich an Code arbeitete, der diese Funktionen mit mehreren Ausgängen mit freiem Speicher hatte. Andernfalls hätte ich nie über diese Techniken gewusst.
Philippe Carphin
1
@thb Außerdem, witzige Geschichte, habe ich meinen damaligen Vorgesetzten als Praktikanten um Erlaubnis gebeten, GOTOs zu verwenden, und ich habe ihm erklärt, dass ich sie auf eine bestimmte Art und Weise verwenden werde, wie sie in den USA verwendet wird Linux-Kernel und er sagten: "Ok, das macht Sinn, und ich wusste auch nicht, dass Sie GOTOs in C verwenden können."
Philippe Carphin
1
@thb Ich weiß nicht, ob es gut ist, in Schleifen zu gehen (anstatt die Schleifen zu brechen) wie diese ? Nun, es ist ein schlechtes Beispiel, aber ich finde, dass die Quicksortierung mit goto-Anweisungen (Beispiel 7a) auf Knuths Structured Programming mit go-Anweisungen nicht sehr nachvollziehbar ist.
Yai0Phah
@ Yai0Phah Ich werde meinen Punkt erklären, aber meine Erklärung mindert nicht dein schönes Beispiel 7a! Ich stimme dem Beispiel zu. Trotzdem halten herrische Studenten gerne Vorträge über goto. Es ist schwierig, eine praktische Anwendung von goto seit 1985 zu finden, die erhebliche Probleme verursacht, wohingegen harmlose gotos gefunden werden können, die die Arbeit des Programmierers erleichtern. Es kommt sowieso in der modernen Programmierung so selten vor, dass ich raten würde, wenn Sie es verwenden möchten, dann sollten Sie es wahrscheinlich nur verwenden. Springen ist in Ordnung. Das Hauptproblem bei goto ist, dass einige glauben, dass das Abwerten von goto sie schlau aussehen lässt.
26.
5

Persönlich würde ich es eher so umgestalten:

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

Das wäre motivierter, wenn man das Deep Nesting vermeidet, als wenn man das Goto vermeidet (IMO ein schlimmeres Problem mit dem ersten Codebeispiel), und wäre natürlich davon abhängig, dass CleanupAfterError außerhalb des Geltungsbereichs möglich ist (in diesem Fall "params") Eine Struktur, die einen zugewiesenen Speicher enthält, den Sie freigeben müssen, eine Datei *, die Sie schließen müssen, oder was auch immer.

Ein wesentlicher Vorteil, den ich bei diesem Ansatz sehe, ist, dass es sowohl einfacher als auch sauberer ist, einen hypothetischen zukünftigen zusätzlichen Schritt zwischen beispielsweise FTCF2 und FTCF3 einzufügen (oder einen vorhandenen aktuellen Schritt zu entfernen), sodass sich die Wartbarkeit (und die Person, die dies tut) verbessern erbt meinen Code, will mich nicht lynchen!) - beiseite, die verschachtelte Version hat das nicht.

Maximus Minimus
quelle
1
Ich habe dies in meiner Frage nicht angegeben, aber es ist möglich, dass die FTCFs NICHT dieselben Parameter haben, was dieses Muster etwas komplizierter macht. Trotzdem danke.
Robz
3

Sehen Sie sich die C-Codierungsrichtlinien der MISRA (Motor Industry Software Reliability Association) an, die es ermöglichen, unter strengen Kriterien zu arbeiten (die Ihr Beispiel erfüllt).

Wo ich arbeite, wird derselbe Code geschrieben - es ist nicht erforderlich - unnötige religiöse Debatten über sie zu vermeiden, ist ein großes Plus in jedem Softwarehaus.

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

oder für "goto in drag" - etwas noch zwielichtigeres als goto, aber umgeht das "no goto ever !!!" camp) "Sicherlich muss es OK sein, verwendet nicht Springen" ....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

Wenn die Funktionen denselben Parametertyp haben, legen Sie sie in eine Tabelle und verwenden Sie eine Schleife.

mattnz
quelle
2
Die aktuellen Richtlinien von MISRA-C: 2004 erlauben keine Weitergabe in irgendeiner Form (siehe Regel 14.4). Beachten Sie, dass das MISRA-Komitee diesbezüglich immer verwirrt war und nicht weiß, auf welchem ​​Fuß es stehen soll. Erstens haben sie die Verwendung von goto, continue usw. bedingungslos verboten. Aber im Entwurf für die kommende MISRA 2011 wollen sie sie wieder zulassen. Bitte beachten Sie, dass MISRA die Zuweisung in if-Anweisungen aus sehr guten Gründen verbietet, da dies weitaus gefährlicher ist als die Verwendung von goto.
1
Aus analytischer Sicht entspricht das Hinzufügen eines Flags zu einem Programm dem Duplizieren des gesamten Code-Codes, in dem sich das Flag im Gültigkeitsbereich befindet, wobei jede if(flag)Kopie den "if" -Zweig und jede entsprechende Anweisung in der anderen Kopie den "if" -Zweig enthält. sonst". Aktionen, die das Flag setzen und löschen, sind wirklich "gotos", die zwischen diesen beiden Versionen des Codes springen. Es gibt Zeiten, in denen die Verwendung von Flags sauberer ist als jede andere Alternative. Das Hinzufügen einer Flagge zum Speichern eines gotoZiels ist jedoch kein guter Kompromiss.
Supercat
1

Ich benutze auch, gotowenn das alternative do/while/continue/breakHackery weniger lesbar wäre.

gotos haben den Vorteil, dass ihre Ziele einen Namen haben und sie lesen goto something;. Dies kann besser lesbar als breakoder continuewenn Sie nicht wirklich stoppen oder etwas fortsetzt.

aib
quelle
4
Überall innerhalb eines do ... while(0)oder eines anderen Konstrukts, bei dem es sich nicht um eine tatsächliche Schleife handelt, sondern um einen systematischen Versuch, die Verwendung von a zu verhindern goto.
aib
1
Ah, danke, ich kannte diese bestimmte Marke von "Warum zum Teufel sollte das jemand tun ?!" konstruiert noch.
Benjamin Kloster
2
Normalerweise wird der Hackery-Befehl do / while / continue / break erst dann unlesbar, wenn das Modul, in dem er enthalten ist, viel zu lang ist.
John R. Strohm
2
Ich kann hier nichts als Rechtfertigung für die Verwendung von goto finden. Pause und Fortsetzung haben eine offensichtliche Konsequenz. wohin gehen? Wo ist das Etikett? Brechen Sie und sagen Sie genau, wo der nächste Schritt ist und in der Nähe.
Rig
1
Das Etikett sollte natürlich innerhalb der Schleife sichtbar sein. Ich stimme dem verdammt langen Teil von @John R. Strohms Kommentar zu. Und Ihr Punkt, übersetzt in Loop-Hackery, lautet "Break out of what? Dies ist keine Loop!". Auf jeden Fall wird dies zu dem, was das OP befürchtet, und ich gebe die Diskussion auf.
AIB
-1
for (int y=0; y<height; ++y) {
    for (int x=0; x<width; ++x) {
        if (find(x, y)) goto found;
    }
}
found:
aib
quelle
Wenn es nur eine Schleife gibt, breakfunktioniert das genauso goto, obwohl es kein Stigma trägt.
9000,
6
-1: Erstens sind x und y bei found: OUT OF SCOPE, das hilft Ihnen also nicht weiter. Zweitens bedeutet die Tatsache, dass Sie mit dem geschriebenen Code bei found angekommen sind, nicht, dass Sie gefunden haben, wonach Sie gesucht haben.
John R. Strohm
Das liegt daran, dass dies das kleinste Beispiel ist, das ich mir für den Fall vorstellen kann, dass mehrere Schleifen durchbrochen werden. Bitte zögern Sie nicht, es zu bearbeiten, um ein besseres Etikett zu erhalten oder eine Prüfung durchzuführen.
AIB
1
Bedenken Sie aber auch, dass C-Funktionen nicht unbedingt nebenwirkungsfrei sind.
AIB
1
@ JohnR.Strohm Das macht nicht viel Sinn ... Das 'found'-Label wird verwendet, um die Schleife zu unterbrechen, nicht um die Variablen zu überprüfen. Wenn ich die Variablen überprüfen wollte, könnte ich Folgendes tun: for (int y = 0; y <height; ++ y) {for (int x = 0; x <width; ++ x) {if (find ( x, y)) {doSomeThingWith (x, y); gehe zu gefunden; }}} gefunden:
YoYoYonnY
-1

Es wird immer Lager geben, die sagen, dass ein Weg akzeptabel ist und ein anderer nicht. Unternehmen, für die ich gearbeitet habe, haben die Stirn gerunzelt oder von der Verwendung dringend abgeraten. Persönlich kann ich mir keine Zeit vorstellen, in der ich eine verwendet habe, aber das bedeutet nicht, dass sie schlecht sind, sondern nur eine andere Art, Dinge zu tun.

In C mache ich normalerweise Folgendes:

  • Auf Bedingungen prüfen, die die Verarbeitung verhindern könnten (fehlerhafte Eingaben usw.) und "return"
  • Führen Sie alle Schritte aus, die eine Ressourcenzuweisung erfordern (z. B. Mallocs).
  • Führen Sie die Verarbeitung durch, wobei mehrere Schritte auf Erfolg prüfen
  • Geben Sie bei erfolgreicher Zuordnung alle Ressourcen frei
  • Ergebnisse zurückgeben

Für die Verarbeitung würde ich anhand Ihres goto-Beispiels Folgendes tun:

error = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }

Es gibt keine Verschachtelung, und innerhalb der if-Klauseln können Sie Fehlerberichte erstellen, wenn der Schritt einen Fehler generiert hat. Es muss also nicht "schlimmer" sein als eine Methode mit gotos.

Ich bin noch nicht auf einen Fall gestoßen, in dem jemand Dinge hat, die mit einer anderen Methode nicht gemacht werden können und die genauso lesbar / verständlich sind, und das ist der Schlüssel, IMHO.

pcm
quelle