为了补充答案,我认为有必要考虑与此相关的相反问题,即:为什么C允许跌倒?
当然,任何编程语言都有两个目标:
为计算机提供指令。
留下程序员意图的记录。
The creation of any programming language is therefore a balance between how to best serve these two goals. On the one hand, the easier it is to turn into computer instructions (whether those are machine code, bytecode like IL, or the instructions are interpreted on execution) then more able that process of compilation or interpretation will be to be efficient, reliable and compact in output. Taken to its extreme, this goal results in our just writing in assembly, IL, or even raw op-codes, because the easiest compilation is where there is no compilation at all.
相反,语言越能表达程序员的意图,而不是为此目的所采取的手段,程序在编写和维护时就越容易理解。
Now, switch could always have been compiled by converting it into the equivalent chain of if-else blocks or similar, but it was designed as allowing compilation into a particular common assembly pattern where one takes a value, computes an offset from it (whether by looking up a table indexed by a perfect hash of the value, or by actual arithmetic on the value*). It's worth noting at this point that today, C# compilation will sometimes turn switch into the equivalent if-else, and sometimes use a hash-based jump approach (and likewise with C, C++, and other languages with comparable syntax).
在这种情况下,允许失败有两个很好的理由:
It just happens naturally anyway: if you build a jump table into a set of instructions, and one of the earlier batches of instructions doesn't contain some sort of jump or return, then execution will just naturally progress into the next batch. Allowing fall-through was what would "just happen" if you turned the switch-using C into jump-table–using machine code.
Coders who wrote in assembly were already used to the equivalent: when writing a jump table by hand in assembly, they would have to consider whether a given block of code would end with a return, a jump outside of the table, or just continue on to the next block. As such, having the coder add an explicit break when necessary was "natural" for the coder too.
因此,在当时,平衡计算机语言的两个目标是合理的尝试,因为它既涉及生成的机器代码,也涉及源代码的表达性。
然而,四十年后,情况不太一样了,原因如下:
Coders in C today may have little or no assembly experience. Coders in many other C-style languages are even less likely to (especially Javascript!). Any concept of "what people are used to from assembly" is no longer relevant.
Improvements in optimisations mean that the likelihood of switch either being turned into if-else because it was deemed the approach likely to be most efficient, or else turned into a particularly esoteric variant of the jump-table approach are higher. The mapping between the higher- and lower-level approaches is not as strong as it once was.
Experience has shown that fall-through tends to be the minority case rather than the norm (a study of Sun's compiler found 3% of switch blocks used a fall-through other than multiple labels on the same block, and it was thought that the use-case here meant that this 3% was in fact much higher than normal). So the language as studied make the unusual more readily catered-to than the common.
Experience has shown that fall-through tends to be the source of problems both in cases where it is accidentally done, and also in cases where correct fall-through is missed by someone maintaining the code. This latter is a subtle addition to the bugs associated with fall-through, because even if your code is perfectly bug-free, your fall-through can still cause problems.
与上述最后两点相关的是,我们可以参考最新版《K&R》中的一段话:
从一种情况失败到另一种情况不是健壮的,当程序被修改时很容易解体。除了对单个计算使用多个标签外,应该谨慎使用并注释fall-贯穿。
作为一种良好的形式,在最后一个case(这里的默认值)后面加上一个break,即使这在逻辑上是不必要的。有一天,当最后添加了另一个案例时,这一点防御性编程将拯救您。
所以,从马的嘴,摔倒在C是有问题的。始终用注释记录错误被认为是一种很好的实践,这是一个普遍原则的应用,即应该记录在哪里做了不寻常的事情,因为这将在以后的代码检查中绊倒,并且/或使您的代码看起来像新手的错误,而实际上它是正确的。
想想看,代码是这样的:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
是在代码中添加一些使fall-through显式的东西,它只是不能被编译器检测到(或其缺失可以被检测到)。
因此,在c#中必须显式地使用fall-through这一事实并不会给那些用其他C风格语言写得很好的人增加任何惩罚,因为他们已经在他们的fall-through中显式了。†
最后,在这里使用goto已经是C和其他类似语言的规范:
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 this sort of case where we want a block to be included in the code executed for a value other than just that which brings one to the preceding block, then we're already having to use goto. (Of course, there are means and ways of avoiding this with different conditionals but that's true of just about everything relating to this question). As such C# built on the already normal way to deal with one situation where we want to hit more than one block of code in a switch, and just generalised it to cover fall-through as well. It also made both cases more convenient and self-documenting, since we have to add a new label in C but can use the case as a label in C#. In C# we can get rid of the below_six label and use goto case 5 which is clearer as to what we are doing. (We'd also have to add break for the default, which I left out just to make the above C code clearly not C# code).
总之:
C# no longer relates to unoptimised compiler output as directly as C code did 40 years ago (nor does C these days), which makes one of the inspirations of fall-through irrelevant.
C# remains compatible with C in not just having implicit break, for easier learning of the language by those familiar with similar languages, and easier porting.
C# removes a possible source of bugs or misunderstood code that has been well-documented as causing problems for the last four decades.
C# makes existing best-practice with C (document fall through) enforceable by the compiler.
C# makes the unusual case the one with more explicit code, the usual case the one with the code one just writes automatically.
C# uses the same goto-based approach for hitting the same block from different case labels as is used in C. It just generalises it to some other cases.
C# makes that goto-based approach more convenient, and clearer, than it is in C, by allowing case statements to act as labels.
总而言之,这是一个非常合理的设计决策
*Some forms of BASIC would allow one to do the likes of GOTO (x AND 7) * 50 + 240 which while brittle and hence a particularly persuasive case for banning goto, does serve to show a higher-language equivalent of the sort of way that lower-level code can make a jump based on arithmetic upon a value, which is much more reasonable when it's the result of compilation rather than something that has to be maintained manually. Implementations of Duff's Device in particular lend themselves well to the equivalent machine code or IL because each block of instructions will often be the same length without needing the addition of nop fillers.
†达夫的装置再次出现在这里,作为一个合理的例外。事实上,在这种模式和类似的模式下,操作的重复使得fall-through的使用相对清晰,即使没有明确的注释。