为什么将switch语句设计为需要中断?


139

给出一个简单的switch语句

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

在情况2中没有break语句,这意味着将在情况3的代码内继续执行。它是这样设计的。为什么做出这个决定?与为块提供自动中断语义相比,这有什么好处?有什么根据?

Answers:


150

许多答案似乎把重点放在为告吹的能力理由要求该break陈述。

我认为这只是一个错误,主要是因为在设计C时,关于如何使用这些构造的经验并不多。

彼得·范德·林登(Peter Van der Linden)在他的《专家C编程》一书中阐述了这一点:

我们分析了Sun C编译器的源代码,以查看使用默认落空率的频率。Sun ANSI C编译器前端有244个switch语句,每个语句平均有7种情况。在所有这些情况中,只有3%发生失败。

换句话说,正常的切换行为有97%的时间是错误的。它不仅仅在编译器中-相反,在此分析中使用掉线的情况通常是发生在编译器中的情况比其他软件中更常见,例如,当编译可具有一个或两个操作数的运算符时:

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

案例失败率被广泛认为是一种缺陷,以至于甚至有一个特殊的注释约定,如上所示,这告诉皮棉“这确实是希望失败率的3%案例之一。”

我认为对于C#来说,在每个case块的末尾都需要一个显式的jump语句是一个好主意(尽管仍然允许堆叠多个case标签-只要只有一个语句块)。在C#中,您仍然可以将一种情况归结为另一种情况-您只需要使用goto

太糟糕了,Java没有抓住机会打破C语义。


确实,我认为它们是为了简化实施。其他一些语言则以效率为代价,支持更复杂的情况(范围,多个值,字符串...)。
披披(FhiLho)

Java可能不想破坏习惯并散布混乱。对于不同的行为,他们将不得不使用不同的语义。无论如何,Java设计人员都失去了许多摆脱C语言的机会。
PhiLho's

@PhiLho-我认为您可能最接近“实施简单”的事实。
Michael Burr

如果您通过GOTO使用显式跳转,仅使用一系列IF语句是否同样有效?
DevinB

3
甚至Pascal都实现了不间断的切换。C编译器程序员怎么不考虑它@@
Chan Le

30

从很多方面来说,c仅仅是标准汇编惯用法的干净接口。在编写由跳转表驱动的流控制时,程序员可以选择掉入或跳出“控制结构”,而跳出则需要一条明确的指令。

所以,c做同样的事情...


1
我知道很多人都这么说,但是我认为这还不是全部。C通常也是装配反图案的丑陋界面。1.丑陋:用语句代替表达式。“声明反映了使用。” 精美的复制粘贴作为模块化和可移植性机制。类型系统方式太复杂;鼓励错误。NULL。(下一条评论继续。)
哈里森(Harrison

2.反模式:不提供“干净的”标记联合,这是程序集的两个基本数据表示惯用法之一。(Knuth,第1卷,第2章)而是“无标签的联合”,这是一种非惯用的技巧。这种选择已经困扰了人们数十年来对数据的思考方式。以- NUL结尾的字符串几乎是有史以来最糟糕的想法。
哈里森

@HarrisonKlaperman:没有任何存储字符串的方法是完美的。如果接受字符串的例程也接受了最大长度的参数,则与以空字符结尾的字符串相关的绝大多数问题都不会发生,而对于将带有长度标记的字符串存储到固定大小的缓冲区而不接受缓冲区大小的参数的例程,则不会发生。在现代人看来,简单地将大小写标签化的switch语句的设计似乎有些奇怪,但这并不比Fortran DO循环的设计更糟。
2012年

如果我决定在汇编中编写一个跳转表,我将获取case值,并神奇地将其转换为跳转表下标,跳转到该位置并执行代码。在那之后,我不会跳到下一种情况。我将跳转到一个统一的地址,这是所有情况的出口。我会跳入下一个案件主体的想法很愚蠢。该模式没有用例。
EvilTeach 2014年

2
如今,人们习惯于清理和自我保护的idos和语言功能,以防止自己陷入脚下,但这是从字节昂贵的地区(C始于1970年前)的再现。如果您的代码需要容纳1024个字节,那么您将面临重用代码段的巨大压力。通过在共享相同端点的不同入口点开始重用代码是实现此目的的一种机制。
rpy

23

要实现Duff的设备,显然:

dsend(to, from, count)
char *to, *from;
int count;
{
    int n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    case 7:      *to = *from++;
    case 6:      *to = *from++;
    case 5:      *to = *from++;
    case 4:      *to = *from++;
    case 3:      *to = *from++;
    case 2:      *to = *from++;
    case 1:      *to = *from++;
               } while (--n > 0);
    }
}

3
我爱达夫的装置。如此优雅,并且邪恶得很快。
2008年

7
是的,但是每次在SO上有switch语句时,都必须显示吗?
billjamesdev

您错过了两个右大括号;-)。
Toon Krijthe

18

如果案例被设计为隐式中断,那么您就不会陷入困境。

case 0:
case 1:
case 2:
    // all do the same thing.
    break;
case 3:
case 4:
    // do something different.
    break;
default:
    // something else entirely.

如果开关被设计为在每种情况下都会隐式爆发,那么您将没有选择。开关盒结构的设计方式使其更加灵活。


12
您可以对一个隐式中断但具有“ fallthrough”关键字的开关进行映像。笨拙,但可行。
dmckee ---前主持人小猫

那会更好吗?我猜想一个case语句比“每个case的代码块”以这种方式工作的更多……这是一个if / then / else块。
billjamesdev

我要补充一点,在这个问题中,每种情况下的范围附件{}都增加了混乱,因为它看起来像“ while”语句的样式。
马修·史密斯

1
@Bill:我认为情况会更糟,但是它将解决Mike B提出的投诉:摔倒(除了多个相同的情况)是罕见的事件,不应作为默认行为。
dmckee ---前主持人小猫,

1
为了简洁起见,我省略了花括号,但应该有多个陈述。我同意,只有在完全相同的情况下才应使用失败率。当案例使用故障转移建立在先前案例的基础上时,这真是令人困惑。
比尔蜥蜴,

15

switch语句中的case语句只是标签。

当您打开一个值时,switch语句实际上会转到具有匹配值的标签。

这意味着必须使用中断,以避免传递到下一个标签下的代码。

至于为什么以这种方式实现它的原因-switch语句的掉线性质在某些情况下会很有用。例如:

case optionA:
    // optionA needs to do its own thing, and also B's thing.
    // Fall-through to optionB afterwards.
    // Its behaviour is a superset of B's.
case optionB:
    // optionB needs to do its own thing
    // Its behaviour is a subset of A's.
    break;
case optionC:
    // optionC is quite independent so it does its own thing.
    break;

2
有两个大问题:1)忘记了break需要的地方。2)如果case语句的顺序更改,则失败可能导致运行错误的case。因此,我发现C#的处理要好得多(goto case除空的大小写标签外,这对于掉线很明显)。
丹尼尔·罗斯

@DanielRose:1)也有一些方法可以break在C#中忘记-最简单的一种方法是,当您不想case做任何事情而忘记添加一个break(也许您被解释性注释中的内容包住了,或者被某些东西打断了)其他任务):执行将落在case下面。2)鼓励goto case是鼓励使用非结构化的“意大利面条”编码-您可能会遇到偶然的循环(case A: ... goto case B; case B: ... ; goto case A;),尤其是当个案在文件中分开且难以合并时。在C ++中,直通是本地化的。
Tony Delroy

8

允许这样的事情:

switch(foo) {
case 1:
    /* stuff for case 1 only */
    if (0) {
case 2:
    /* stuff for case 2 only */
    }
    /* stuff for cases 1 and 2 */
case 3:
    /* stuff for cases 1, 2, and 3 */
}

case关键字视为goto标签,它自然而然地出现了。


8
if(0)在第一种情况的末尾那是邪恶的,只有在目标是混淆代码的情况下才应使用。
丹尼尔·罗斯

11
我认为,这整个答案是作恶的一种练习。:-)
R .. GitHub停止帮助ICE

3

当几种情况需要执行相同的代码(或依次执行相同的代码)时,它消除了代码重复。

由于在汇编语言级别上,无论您是否在每个语言之间都中断,都不会在乎,总而言之,零失误案例的开销为零,所以为什么不让他们这样做,因为它们在某些情况下具有明显的优势。


2

我碰巧遇到了将向量中的值分配给结构的情况:必须以这种方式完成:如果数据向量短于结构中数据成员的数量,则其余成员将保留在它们的默认值。在那种情况下,省略break是非常有用的。

switch (nShorts)
{
case 4: frame.leadV1    = shortArray[3];
case 3: frame.leadIII   = shortArray[2];
case 2: frame.leadII    = shortArray[1];
case 1: frame.leadI     = shortArray[0]; break;
default: TS_ASSERT(false);
}

0

正如这里许多人所指定的,它允许单个代码块在多种情况下都可以工作。这应该与您在示例中指定的“每个案例的代码块”相比,对于switch语句,是更常见的情况。

如果每种情况都有一个代码块而不会掉线,则也许应该考虑使用if-elseif-else块,因为这似乎更合适。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.