427 Stimmen

Switch-Anweisung fallthrough in C#?

Das Durchfallen von Schaltanweisungen ist einer der Hauptgründe, warum ich persönlich die switch vs. if/else if Konstrukte. Ein Beispiel ist hier angebracht:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Die klugen Leute schrecken zurück, weil die string[] s sollten außerhalb der Funktion deklariert werden: Nun, sie sind es, dies ist nur ein Beispiel.

Der Compiler schlägt mit dem folgenden Fehler fehl:

Control cannot fall through from one case label ('case 3:') to another
Control cannot fall through from one case label ('case 2:') to another

Warum? Und gibt es eine Möglichkeit, diese Art von Verhalten zu erreichen, ohne dass man drei if s?

746voto

Alex Lyman Punkte 15257

(Kopieren/Einfügen eines Antwort, die ich an anderer Stelle gegeben habe )

Durchfallend switch - case s kann erreicht werden, indem kein Code in einer case (siehe case 0 ), oder die Verwendung des speziellen goto case (siehe case 1 ) o goto default (siehe case 2 ) bildet:

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}

56voto

Jon Skeet Punkte 1325502

Das "Warum" besteht darin, ein versehentliches Durchfallen zu vermeiden, wofür ich dankbar bin. Dies ist eine nicht seltene Quelle von Fehlern in C und Java.

Die Abhilfe ist die Verwendung von goto, z. B.

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

Das allgemeine Design von Schalter/Gehäuse ist meiner Meinung nach etwas unglücklich. Es ist zu sehr an C angelehnt - es gibt einige nützliche Änderungen, die in Bezug auf Scoping usw. vorgenommen werden könnten. Ein intelligenterer Schalter, der Mustervergleiche usw. durchführen könnte, wäre wohl hilfreich, aber das ist wirklich eine Änderung von Schalter zu "prüfe eine Folge von Bedingungen" - an diesem Punkt wäre vielleicht ein anderer Name angebracht.

32voto

Jon Hanna Punkte 106367

Um die Antworten zu ergänzen, lohnt es sich meines Erachtens, in diesem Zusammenhang auch die umgekehrte Frage zu stellen, nämlich: Warum hat C das Durchfallen überhaupt zugelassen?

Jede Programmiersprache dient natürlich zwei Zielen:

  1. Geben Sie dem Computer Anweisungen.
  2. Hinterlassen Sie einen Hinweis auf die Absichten des Programmierers.

Die Entwicklung einer jeden Programmiersprache ist daher eine Abwägung, wie diese beiden Ziele am besten erreicht werden können. Je einfacher die Umsetzung in Computerbefehle ist (unabhängig davon, ob es sich um Maschinencode, Bytecode wie AWL handelt oder ob die Befehle bei der Ausführung interpretiert werden), desto effizienter, zuverlässiger und kompakter ist der Prozess der Kompilierung oder Interpretation. Im Extremfall führt dieses Ziel dazu, dass wir nur noch in Assembler, AWL oder sogar in rohen Op-Codes schreiben, denn die einfachste Kompilierung ist die, bei der es überhaupt keine Kompilierung gibt.

Umgekehrt ist das Programm sowohl beim Schreiben als auch bei der Wartung umso verständlicher, je mehr die Sprache die Absicht des Programmierers ausdrückt und nicht nur die Mittel, die zu diesem Zweck eingesetzt werden.

Jetzt, switch hätte immer kompiliert werden können, indem man sie in die entsprechende Kette von if-else Blöcke oder ähnliches, aber es wurde entwickelt, um die Kompilierung in ein bestimmtes gemeinsames Assembler-Muster zu ermöglichen, bei dem man einen Wert nimmt und daraus einen Offset berechnet (entweder durch Nachschlagen in einer Tabelle, die durch einen perfekten Hash des Wertes indiziert ist, oder durch tatsächliche Arithmetik auf dem Wert*). Es ist erwähnenswert an dieser Stelle, dass heute, C# Kompilierung wird manchmal drehen switch in das Äquivalent if-else und verwenden manchmal einen Hash-basierten Sprungansatz (ebenso wie C, C++ und andere Sprachen mit vergleichbarer Syntax).

In diesem Fall gibt es zwei gute Gründe für die Zulassung des Durchfallens:

  1. Dies geschieht ohnehin auf natürliche Weise: Wenn Sie eine Sprungtabelle in eine Reihe von Befehlen einbauen und einer der früheren Stapel von Befehlen keine Art von Sprung oder Rücksprung enthält, wird die Ausführung einfach auf natürliche Weise in den nächsten Stapel übergehen. Das Zulassen von Fall-Through war das, was "einfach passieren" würde, wenn man die switch -verwendenden C in einen Sprungtabellen-verwendenden Maschinencode.

  2. Programmierer, die in Assembler geschrieben haben, waren bereits an das Äquivalent gewöhnt: Wenn sie eine Sprungtabelle von Hand in Assembler schrieben, mussten sie überlegen, ob ein bestimmter Codeblock mit einem Return, einem Sprung außerhalb der Tabelle oder einfach mit dem nächsten Block enden würde. Wenn der Programmierer also eine explizite break wenn nötig, war auch für den Programmierer "natürlich".

Zum damaligen Zeitpunkt war es daher ein vernünftiger Versuch, die beiden Ziele einer Computersprache in Bezug auf den erzeugten Maschinencode und die Ausdruckskraft des Quellcodes miteinander in Einklang zu bringen.

Vier Jahrzehnte später sind die Dinge jedoch nicht mehr ganz so wie früher, und zwar aus mehreren Gründen:

  1. Programmierer, die heute in C programmieren, haben möglicherweise wenig oder gar keine Erfahrung mit Assembler. Programmierer in vielen anderen C-ähnlichen Sprachen haben das noch weniger (insbesondere Javascript!). Das Konzept "was die Leute von Assembler gewohnt sind" ist nicht mehr relevant.
  2. Verbesserungen bei den Optimierungen bedeuten, dass die Wahrscheinlichkeit, dass switch die entweder in if-else weil er als der wahrscheinlich effizienteste Ansatz angesehen wurde oder sich in eine besonders esoterische Variante des Jump-Table-Ansatzes verwandelte, sind höher. Die Zuordnung zwischen den über- und untergeordneten Ansätzen ist nicht mehr so stark wie früher.
  3. Die Erfahrung hat gezeigt, dass Fall-Through eher die Minderheit als die Norm ist (eine Studie über den Compiler von Sun ergab, dass 3 % der switch Blöcken ein anderes Fall-through-Verfahren als mehrere Etiketten auf demselben Block verwendet, und es wurde angenommen, dass der Anwendungsfall hier bedeutete, dass diese 3 % tatsächlich viel höher als normal waren). Die untersuchte Sprache macht es also leichter, auf das Ungewöhnliche einzugehen als auf das Übliche.
  4. Die Erfahrung hat gezeigt, dass Fall-Through sowohl in Fällen, in denen es versehentlich durchgeführt wird, als auch in Fällen, in denen der korrekte Fall-Through von jemandem, der den Code pflegt, übersehen wird, die Quelle von Problemen ist. Letzteres ist ein subtiler Zusatz zu den Fehlern, die mit Fall-Through verbunden sind, denn selbst wenn Ihr Code vollkommen fehlerfrei ist, kann Ihr Fall-Through dennoch Probleme verursachen.

Im Zusammenhang mit den letzten beiden Punkten sei das folgende Zitat aus der aktuellen Ausgabe von K&R angeführt:

Der Übergang von einem Fall zum anderen ist nicht robust, da er bei einer Änderung des Programms leicht zerfällt. Mit Ausnahme von mehreren Labels für eine einzige Berechnung sollten Fall-Throughs sparsam verwendet und kommentiert werden.

Der guten Ordnung halber sollten Sie nach dem letzten Fall einen Umbruch einfügen, auch wenn dieser logisch nicht notwendig ist. Eines Tages, wenn ein weiterer Fall am Ende hinzugefügt wird, wird dieses Stück defensiver Programmierung Sie retten.

Aus dem Mund des Pferdes ist das Durchfallen in C also problematisch. Es gilt als gute Praxis, Fall-Throughs immer mit Kommentaren zu dokumentieren, was eine Anwendung des allgemeinen Grundsatzes ist, dass man dokumentieren sollte, wo man etwas Ungewöhnliches tut, denn das ist es, was eine spätere Untersuchung des Codes stören wird und/oder Ihren Code so aussehen lässt, als ob er einen Anfängerfehler enthält, obwohl er in Wirklichkeit korrekt ist.

Und wenn man darüber nachdenkt, ist das ein Code wie dieser:

switch(x)
{
  case 1:
   foo();
   /* FALLTHRU */
  case 2:
    bar();
    break;
}

Ist Wenn man etwas hinzufügt, um das Durchfallen im Code explizit zu machen, ist es einfach nicht etwas, das vom Compiler erkannt werden kann (oder dessen Fehlen erkannt werden kann).

Die Tatsache, dass man in C# explizit mit Fall-Through arbeiten muss, bedeutet also keinen Nachteil für Leute, die ohnehin gut in anderen Sprachen im C-Stil geschrieben haben, da sie ihre Fall-Throughs bereits explizit verwenden.

Schließlich ist die Verwendung von goto ist hier bereits eine Norm aus C und anderen derartigen Sprachen:

switch(x)
{
  case 0:
  case 1:
  case 2:
    foo();
    goto below_six;
  case 3:
    bar();
    goto below_six;
  case 4:
    baz();
    /* FALLTHRU */
  case 5:
  below_six:
    qux();
    break;
  default:
    quux();
}

In solchen Fällen, in denen wir einen Block in den Code einbeziehen wollen, der für einen anderen Wert ausgeführt wird als nur den, der einen zum vorhergehenden Block bringt, müssen wir bereits goto . (Natürlich gibt es Mittel und Wege, dies mit verschiedenen Konditionalen zu vermeiden, aber das gilt für so ziemlich alles, was mit dieser Frage zu tun hat). So baute C# auf die bereits übliche Art und Weise auf, mit einer Situation umzugehen, in der wir mehr als einen Codeblock in einem switch und verallgemeinerte sie einfach, um auch das Durchfallen abzudecken. Dadurch wurden auch beide Fälle bequemer und selbstdokumentierend, da wir in C ein neues Label hinzufügen müssen, aber die case als Bezeichnung in C#. In C# können wir uns von dem below_six Kennzeichnung und Verwendung goto case 5 was deutlicher macht, was wir tun. (Wir müssten auch hinzufügen break für die default , die ich weggelassen habe, um den obigen C-Code eindeutig von C#-Code zu unterscheiden).

Zusammengefasst bedeutet dies:

  1. C# bezieht sich nicht mehr so direkt auf die nicht optimierte Compilerausgabe wie C-Code vor 40 Jahren (und C heutzutage auch nicht), was eine der Inspirationen für Fall-Through irrelevant macht.
  2. C# bleibt mit C kompatibel, da es nicht nur implizite break für ein leichteres Erlernen der Sprache durch diejenigen, die mit ähnlichen Sprachen vertraut sind, und eine leichtere Portierung.
  3. C# beseitigt eine mögliche Quelle von Fehlern oder missverstandenem Code, die seit vier Jahrzehnten als problematisch bekannt ist.
  4. C# macht bestehende Best-Practice mit C (document fall through) durch den Compiler erzwingbar.
  5. In C# ist der ungewöhnliche Fall derjenige mit explizitem Code, der übliche Fall derjenige mit dem Code, den man einfach automatisch schreibt.
  6. C# verwendet die gleiche goto -basierten Ansatz für das Treffen desselben Blocks von verschiedenen case Bezeichnungen, wie sie in C verwendet werden. Sie verallgemeinert sie nur auf einige andere Fälle.
  7. C# macht das goto -basierten Ansatz bequemer und klarer als in C, da er es ermöglicht case Anweisungen, die als Beschriftungen dienen.

Alles in allem eine ziemlich vernünftige Entscheidung.


*Einige Formen von BASIC erlauben es, Dinge zu tun wie GOTO (x AND 7) * 50 + 240 die zwar brüchig und daher ein besonders überzeugendes Argument für ein Verbot sind goto dient dazu, ein höhersprachiges Äquivalent der Art und Weise zu zeigen, wie Code auf niedrigerer Ebene einen Sprung auf der Grundlage von Arithmetik auf einen Wert machen kann, was viel vernünftiger ist, wenn es das Ergebnis der Kompilierung ist und nicht etwas, das manuell gepflegt werden muss. Implementierungen von Duff's Device eignen sich besonders gut für den äquivalenten Maschinencode oder AWL, weil jeder Block von Anweisungen oft die gleiche Länge hat, ohne dass die Addition von nop Füllstoffe.

†Duff's Device taucht hier wieder auf, als eine vernünftige Ausnahme. Die Tatsache, dass es bei diesem und ähnlichen Mustern eine Wiederholung von Operationen gibt, macht die Verwendung von Fall-Through auch ohne ausdrücklichen Hinweis relativ deutlich.

27voto

Coincoin Punkte 26516

Switch Fallthrough ist historisch gesehen eine der Hauptquellen von Fehlern in modernen Softwares. Der Sprachdesigner hat beschlossen, den Sprung am Ende des Falles zwingend vorzuschreiben, es sei denn, man springt direkt zum nächsten Fall, ohne ihn zu bearbeiten.

switch(value)
{
    case 1:// this is still legal
    case 2:
}

18voto

kenny Punkte 20102

Sie können 'goto case label' http://www.blackwasp.co.uk/CSharpGoto.aspx

Die goto-Anweisung ist ein einfacher Befehl, der die Kontrolle des Programms bedingungslos an eine andere Anweisung überträgt. Der Befehl wird oft kritisiert und einige Entwickler plädieren dafür, ihn aus allen höheren Programmiersprachen zu entfernen, da er zu folgenden Problemen führen kann Spaghetticode . Dies geschieht, wenn es so viele goto-Anweisungen oder ähnliche Sprunganweisungen gibt, dass der Code schwer zu lesen und zu pflegen ist. Es gibt jedoch Programmierer, die darauf hinweisen, dass die goto-Anweisung, wenn sie sorgfältig eingesetzt wird, eine elegante Lösung für einige Probleme bietet...

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X