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:
- Geben Sie dem Computer Anweisungen.
- 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:
-
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.
-
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:
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- C# beseitigt eine mögliche Quelle von Fehlern oder missverstandenem Code, die seit vier Jahrzehnten als problematisch bekannt ist.
- C# macht bestehende Best-Practice mit C (document fall through) durch den Compiler erzwingbar.
- In C# ist der ungewöhnliche Fall derjenige mit explizitem Code, der übliche Fall derjenige mit dem Code, den man einfach automatisch schreibt.
- 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.
- 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.