为了补充这里的答案,我认为值得考虑与此相关的相反问题,即。为什么 C 一开始就允许掉线?
任何编程语言当然都有两个目标:
- 向计算机提供指令。
- 留下程序员的意图记录。
因此,任何编程语言的创建都是如何最好地服务于这两个目标之间的平衡。一方面,越容易转化为计算机指令(无论是机器码、像 IL 一样的字节码,还是在执行时解释的指令),那么编译或解释过程将更高效、可靠和输出紧凑。极端情况下,这个目标导致我们只用汇编、IL 甚至原始操作码编写,因为最简单的编译是根本没有编译的地方。
相反,语言越多地表达程序员的意图,而不是为此而采取的手段,程序在编写和维护期间就越容易理解。
现在,switch 始终可以通过将其转换为 if-else 块或类似块的等效链来编译,但它被设计为允许编译为特定的通用汇编模式,其中一个取值,计算偏移量从中(无论是通过查找由值的完美哈希索引的表,还是通过对值的实际算术*)。在这一点上值得注意的是,今天,C# 编译有时会将switch 转换为等效的if-else,有时会使用基于哈希的跳转方法(对于 C、C++ 和其他具有类似语法的语言也是如此)。
在这种情况下,允许掉线有两个很好的理由:
-
无论如何它都会自然而然地发生:如果您将跳转表构建成一组指令,并且较早的一批指令中的一个不包含某种跳转或返回,那么执行将自然地进入下一批.如果您将 switch-using C 转换为 jump-table-using machine code,则允许失败是“刚刚发生”的事情。
-
用汇编编写代码的程序员已经习惯了类似的情况:在汇编中手动编写跳转表时,他们必须考虑给定的代码块是否会以返回、跳出表的方式结束,或者继续前进到下一个街区。因此,让编码员在必要时添加明确的break 对编码员来说也是“自然的”。
因此,在当时,平衡计算机语言的两个目标是一种合理的尝试,因为它与生成的机器代码和源代码的表达性有关。
然而,四年过去了,情况并不完全一样,原因如下:
- 如今的 C 语言编码人员可能很少或根本没有汇编经验。许多其他 C 风格语言的编码人员更不可能(尤其是 Javascript!)。任何“人们习惯于组装什么”的概念都不再相关。
- 优化方面的改进意味着
switch 被转换为if-else 的可能性更高,因为它被认为可能是最有效的方法,或者转换为跳表方法的一个特别深奥的变体的可能性更高。高级和低级方法之间的映射不像以前那么强了。
- 经验表明,失败往往是少数情况而不是常态(Sun 编译器的一项研究发现,
switch 块中有 3% 使用了失败而不是同一块上的多个标签,并且它被认为这里的用例意味着这 3% 实际上远高于正常水平)。因此,所研究的语言使不寻常的语言比普通语言更容易迎合。
- 经验表明,无论是在意外完成的情况下,还是在维护代码的人错过正确的失败的情况下,失败往往都是问题的根源。后者是对与失败相关的错误的微妙补充,因为即使您的代码完全没有错误,您的失败仍然会导致问题。
关于最后两点,请考虑当前版本的 K&R 中的以下引用:
从一种情况到另一种情况并不稳健,在修改程序时容易解体。除了单个计算的多个标签外,应谨慎使用失败并加以注释。
作为一种良好的形式,在最后一种情况(这里的默认值)之后放置一个中断,即使它在逻辑上是不必要的。有一天,当最后添加另一个案例时,这一点防御性编程将拯救你。
因此,从马的口中,C 中的失败是有问题的。始终使用 cmets 记录失败被认为是一种很好的做法,这是一个一般原则的应用,即应该记录一个人在哪里做了一些不寻常的事情,因为这会导致以后检查代码和/或使你的代码看起来像它当它实际上是正确的时,它有一个新手的错误。
当你想到它时,代码如下:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
正在添加一些内容以在代码中显式地通过,它只是无法被编译器检测到(或无法检测到)。
因此,在 C# 中,on 必须与 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();
}
在这种情况下,如果我们希望将一个块包含在执行的代码中,而不是仅仅为前一个块带来一个值,那么我们已经不得不使用goto。 (当然,有一些方法可以通过不同的条件来避免这种情况,但与这个问题有关的所有事情都是如此)。因此,C# 建立在已经很正常的方式上来处理一种情况,即我们想要在switch 中命中多个代码块,并且只是将其概括为也涵盖了失败。它还使这两种情况更方便和自我记录,因为我们必须在 C 中添加一个新标签,但可以使用 case 作为 C# 中的标签。在 C# 中,我们可以去掉below_six 标签并使用goto case 5,这样更清楚我们在做什么。 (我们还必须为default 添加break,我省略了只是为了使上面的C 代码明显不是C# 代码)。
因此总结如下:
- C# 不再像 40 年前的 C 代码那样直接与未优化的编译器输出相关(现在 C 也如此),这使得 fall-through 的灵感之一变得无关紧要。
- C# 与 C 保持兼容,不仅具有隐式
break,便于熟悉类似语言的人学习该语言,也更易于移植。
- C# 消除了可能的错误或误解代码的来源,这些代码在过去四年中已被充分记录为导致问题的原因。
- C# 使现有的 C 最佳实践(文档失败)可由编译器强制执行。
- C# 使不寻常的情况具有更明确的代码,通常情况下具有代码的情况只是自动编写。
- C# 使用与 C 中相同的基于
goto 的方法从不同的 case 标签点击相同的块。它只是将其推广到其他一些情况。
- C# 允许
case 语句充当标签,使基于 goto 的方法比 C 中的方法更方便、更清晰。
总而言之,一个非常合理的设计决策
*某些形式的 BASIC 将允许人们执行 GOTO (x AND 7) * 50 + 240 之类的操作,虽然这很脆弱,因此是禁止 goto 的一个特别有说服力的案例,但确实有助于展示一种高级语言等价物级代码可以基于值的算术进行跳转,当它是编译的结果而不是必须手动维护的东西时,这更合理。 Duff 设备的实现尤其适用于等效的机器代码或 IL,因为每个指令块通常具有相同的长度,而无需添加 nop 填充符。
†Duff 的设备再次出现在这里,作为一个合理的例外。使用该模式和类似模式,存在重复操作这一事实有助于使 fall-through 的使用相对清晰,即使没有对此效果的明确注释。