为什么我们必须使用中断开关?


74

谁决定(并基于什么概念)在每个语句中switch必须使用(多种语言)构造break

我们为什么要写这样的东西:

switch(a)
{
    case 1:
        result = 'one';
        break;
    case 2:
        result = 'two';
        break;
    default:
        result = 'not determined';
        break;
}

(在PHP和JS中注意到这一点;可能有许多其他语言正在使用此语言)

如果switch是的替代选择if,为什么我们不能使用与相同的构造if?即:

switch(a)
{
    case 1:
    {
        result = 'one';
    }
    case 2:
    {
        result = 'two';
    }
    default:
    {
        result = 'not determined';
    }
}

据说这break阻止了当前块之后的块的执行。但是,是否有人真的遇到了需要执行当前块及其后续块的情况?我没有 对我来说,break一直在那里。在每个街区。在每个代码中。


1
正如大多数答案所指出的那样,这与“掉线”有关,其中多个条件通过同一“ then”块路由。但是,这取决于语言-例如,RPG将一条CASE语句等同于一个巨型if / elseif块。
发条博物馆

34
这是C设计师的错误决定(就像许多决定一样,它是为了使从汇编-> C的转换和向后转换的转换更容易,而不是为了易于编程而做出),然后不幸地被继承了。其他基于C的语言。使用简单的规则“始终将常见情况设置为默认!!” ,我们可以看到默认行为应该是打破常规,并使用一个单独的关键字进行突破,因为这是迄今为止最常见的情况。
BlueRaja-Danny Pflughoeft

4
我曾经多次使用过失败,尽管可以说在(C)时使用我首选语言的switch语句可以避免这种情况。例如我做过case 'a': case 'A': case 'b': case 'B'但主要是因为我做不到case in [ 'a', 'A', 'b', 'B' ]。一个更好的问题是,在我当前的首选语言(C#)中,中断是 强制性的,并且没有隐式的落空;遗忘break是语法错误...:\
KutuluMike,2012年

1
解析器实现中经常会出现与实际代码的冲突。case TOKEN_A: /*set flag*/; case TOKEN_B: /*consume token*/; break; case TOKEN_C: /*...*/
OrangeDog 2012年

1
@ BlueRaja-DannyPflughoeft:C的设计也易于编译。“如果发出跳跃break是显著更简单,而不是实施规则存在的任何地方”,“如果不发出跳跃fallthrough存在于switch”。
乔恩·普迪

Answers:


95

C是最早使用switch这种形式的语句的语言之一,所有其他主要语言都从那里继承了该语句,主要是选择保留默认的C语义-他们要么没有想到更改它的优点,要么断定它们比保持每个人习惯的行为重要。

至于为什么以这种方式设计C,它可能源于C作为“便携式组件”的概念。该switch语句基本上是分支表的抽象,并且分支表也具有隐式下降,并且需要附加的跳转指令来避免它。

因此,基本上,C的设计人员选择保留默认的汇编器语义。


3
VB.NET是确实对其进行了更改的一种语言示例。它没有流入case子句的危险。
戳2012年

1
Tcl的另一种语言永远不会出现在下一个子句中。几乎从不希望这样做(与C语言不同,在编写某些类型的程序时C语言非常有用)。
Donal Fellows

4
C的祖先语言B和较旧的BCPL都具有(必须?)switch语句;BCPL拼写了它switchon
基思·汤普森

Ruby的case语句不会失败;您可以通过在一个测试中包含两个测试值来获得类似的行为when
内森·朗

86

因为switch不是if ... else这些语言中语句的替代方法。

通过使用switch,我们可以一次匹配多个条件,在某些情况下,我们将不胜感激。

例:

public Season SeasonFromMonth(Month month)
{
    Season season;
    switch (month)
    {
        case Month.December:
        case Month.January:
        case Month.February:
            season = Season.Winter;
            break;

        case Month.March:
        case Month.April:
        case Month.May:
            season = Season.Spring;
            break;

        case Month.June:
        case Month.July:
        case Month.August:
            season = Season.Summer;
            break;

        default:
            season = Season.Autumn;
            break;
    }

    return season;
}

15
more than one block ... unwanted in most cases我不同意你的看法。
Satish Pandey 2012年

2
@DanielB但这取决于语言;C#switch语句不允许这种可能性。
安迪

5
@Andy我们还在谈论失败吗?C#switch语句肯定允许它(请查看第三个示例),因此需要break。但是,据我所知,Duff的设备无法在C#中运行。
丹尼尔·B

8
@DanielB是的,但是编译器不允许一个条件,一个代码,然后再一个条件,而不会中断。您可以将多个条件堆叠在一起,而之间没有任何代码,或者您需要休息一下。从您的链接C# does not support an implicit fall through from one case label to another。您可以尝试一下,但绝不会意外忘记休息。
安迪

3
@Matsemann ..我现在可以使用来完成所有工作,if..else但在某些情况下switch..case会更可取。如果我们使用if-else,上面的示例将有些复杂。
Satish Pandey

15

在C的上下文中,这已经在Stack Overflow上被问到了:为什么switch语句设计为需要中断?

总结接受的答案,可能是一个错误。大多数其他语言可能都只跟在C之后。但是,诸如C#之类的某些语言似乎已通过允许失败来解决此问题-但只有在程序员明确地这样说时,才能解决此问题(来源:上面的链接,我自己并不说C#) 。


Google Go执行IIRC相同的操作(如果明确指定,则允许失败)。
狮子座

PHP也可以。而且,您对结果代码的了解越多,它会使您感到不舒服。我喜欢/* FALLTHRU */上面@Tapio链接的答案中的评论,以证明您的意思。
DaveP

1
goto case像链接一样,说C#具有并不能说明为什么它是如此有效。您仍然可以像上面那样堆叠多个案例,但是一旦您插入代码,就必须有一个明确的条件goto case whatever才能进入下一个案例,否则您将获得编译器错误。如果您问我,在这两种情况中,与不使用启用显式穿透相比,能够堆叠案例而不必担心由于疏忽而导致的意外穿透是一种更好的功能goto case
Jesper 2012年

9

我将举一个例子。如果要列出一年中每个月的天数,很明显,有些月份有31、30和1 28/29。看起来像这样

switch(month) {
    case 4:
    case 6:
    case 9:
    case 11;
        days = 30;
        break;
    case 2:
        //Find out if is leap year( divisible by 4 and all that other stuff)
        days = 28 or 29;
        break;
    default:
        days = 31;
}

这是一个示例,其中多个案例具有相同的效果,并且全部组合在一起。显然有一个选择break关键字的原因,而不是 if ... else if构造的原因。

这里要注意的主要事情是,具有许多类似情况的switch语句不是if ... else if ... else if ... else每个情况,而是if(1、2、3) ...否则if(4,5,6)else ...


2
您的示例看起来比if没有任何好处的情况更为冗长。也许,如果您的示例分配了名称或除掉线外还进行了特定于月份的工作,掉线的效用会更明显吗?(无需评论是否应该放在第一位)
horatio 2012年

1
那是真实的。但是,我只是试图展示可以使用掉线的情况。如果每个月需要单独完成某件事,那显然会更好。
Awemo

8

在两种情况下,可能会发生从一个案例到另一个案例的“失败”的情况-空案例:

switch ( ... )
{
  case 1:
  case 2:
    do_something_for_1_and_2();
    break;
  ...
}

和非空的情况

switch ( ... )
{
  case 1:
    do_something_for_1();
    /* Deliberately fall through */
  case 2:
    do_something_for_1_and_2();
    break;
...
}

尽管引用了Duff的设备,但第二种情况的合法实例很少且相差甚远,并且通常被编码标准禁止并在静态分析期间进行标记。在哪里找到它,通常是由于省略了break

前者是完全明智和普遍的。

老实说,我认为没有理由需要breakand语言解析器知道一个空的case-body是一个失败,而一个非空的case是独立的。

遗憾的是,ISO C小组似乎更着重于向语言添加新的(不需要的)和定义错误的功能,而不是修复未定义的,未指定的或实现定义的功能,更不用说是不合逻辑的了。


2
谢天谢地,ISO C并未对语言进行重大更改!
杰克·艾德利

4

不强行使用break可能会导致本来很难完成的许多事情。其他人则注意到分组案例,对于这些案例,有许多非平凡的案例。

迫切需要break不要使用的一种情况是Duff's Device。这用于“ 展开 ”循环,在该循环中可以通过限制所需的比较次数来加快操作速度。我相信,最初使用时允许使用的功能,以前使用完整的汇总循环过慢。在某些情况下,它以代码大小为代价来交换速度。

break如果案例包含任何代码,则最好使用适当的注释来代替。否则,有人会修复丢失的内容break并引入错误。


感谢Duff的装置范例(+1)。我没有意识到。
trejder 2014年

3

在C中,起源似乎是这样的,该switch语句的代码块不是特殊的构造。它是一个普通的代码块,就像if语句下的块一样。

switch ()
{
}

if ()
{
}

casedefault是此块内的跳转标签,具体与关联switch。它们的处理方式与的正常跳转标签相同goto。这里有一个重要的特定规则:跳转标签几乎可以在代码中的任何位置出现,而不会中断代码流。

作为普通的代码块,它不必是复合语句。标签也是可选的。这些是switchC语言中的有效语句:

switch (a)
    case 1: Foo();

switch (a)
    Foo();

switch (a)
{
    Foo();
}

以C标准本身为例(6.8.4.2):

switch (expr) 
{ 
    int i = 4; 
    f(i); 
  case 0: 
    i=17; 
    /*falls through into default code */ 
  default: 
    printf("%d\n", i); 
} 

在人工程序片段中,标识符为i的对象存在自动存储持续时间(在该块内),但从未初始化,因此,如果控制表达式的值为非零值,则对printf函数的调用将访问不确定的值。同样,无法调用函数f。

此外,default也是跳转标签,因此可以位于任何地方,而不必是最后一种情况。

这也解释了达夫的装置:

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);
}

为什么会失败?因为在代码正常块正常码流,下通到下面的语句是期待,就像你希望它在一个if代码块。

if (a == b)
{
    Foo();
    /* "Fall-through" to Bar expected here. */
    Bar();
}

switch (a)
{
    case 1: 
        Foo();
        /* Automatic break would violate expected code execution semantics. */
    case 2:
        Bar();
}

我的猜测是这样做的原因是易于实施。您不需要特殊的代码即可解析和编译switch块,而无需担心特殊的规则。您只需像解析其他任何代码一样解析它,只需关心标签和跳转选择。

所有这些有趣的后续问题是以下嵌套语句是否显示“完成”。或不。

int a = 10;

switch (a)
{
    switch (a)
    {
        case 10: printf("Done.\n");
    }
}

C标准对此予以关注(6.8.4.2.4):

案例或默认标签只能在最接近的封闭switch语句中访问。


1

几个人已经提到了匹配多个条件的概念,这有时会非常有价值。但是,匹配多个条件的能力并不一定要求您对每个匹配的条件进行完全相同的操作。考虑以下:

switch (someCase)
{
    case 1:
    case 2:
        doSomething1And2();
        break;

    case 3:
        doSomething3();

    case 4:
        doSomething3And4();
        break;

    default:
        throw new Error("Invalid Case");
}

这里有两种不同的方式来匹配多个条件。在条件1和2的情况下,它们只是陷入了完全相同的代码图中,并且做着完全相同的事情。随着条件3和4,但是,虽然他们最终都通过调用doSomething3And4()只有 3个电话doSomething3()


迟到派对,但这绝对不是函数名称所暗示的“ 1和2”:三种不同的状态都可以导致case 2触发代码,因此如果我们想要正确(并且做到)函数名称必须是“ doSomethingBecause_1_OR_2_OR_1AND2()或”(尽管不可变的匹配两个不同情况的合法代码实际上仍然不存在,但实际上至少不存在,因此至少应该是这样doSomethingBecause1or2()
Mike'Pomax'Kamermans

换句话说,doSomething[ThatShouldBeDoneInTheCaseOf]1And[InTheCaseOf]2()。它不是指逻辑和/或。
Panzercrisis

我知道不是,但是鉴于人们为什么会对这种代码感到困惑,它应该绝对解释“和”是什么,因为依靠某人来猜测“自然和”与“逻辑目的”的答案才行得通精确解释时,需要在答案中进行更多解释,或者重写函数名称以消除歧义=)
Mike'Pomax'Kamermans

1

回答您的两个问题。

为什么C需要休息?

它归结为Cs的“便携式汇编程序”。像这样的伪代码很常见:

    targets=(addr1,addr2,addr3);
    opt = 1  ## or 0 or 2
switch:
    br targets[opt]  ## go to addr2 
addr1:
    do 1stuff
    br switchend
addr2:
    do 2stuff
    br switchend
addr3
    do 3stuff
switchend:
    ......

switch语句旨在提供更高级别的类似功能。

我们有没有不间断的开关吗?

是的,这很普遍,并且有一些用例。

首先,您可能想在几种情况下采取相同的措施。为此,我们将箱子彼此堆叠:

case 6:
case 9:
    // six or nine code

状态机中另一个常见的用例是,在处理一个状态之后,我们立即要进入并处理另一个状态:

case 9:
    crashed()
    newstate=10;
case 10:
    claim_damage();
    break;

-2

Break语句是一个跳转语句,允许用户在进行,执行,执行或执行时退出最近的封闭开关(针对您的情况)。就这么简单。


-3

我认为以上所有内容(该线程的主要结果是,如果您要设计一种新语言),默认值是无需添加break语句,编译器将像对待该语句一样对待它。

如果您希望继续进行下一种情况的罕见情况-只需continue声明即可。

可以对此进行改进,以便仅在案例内使用大括号时才继续进行,这样具有多个案例的几个月来执行相同精确代码的示例始终可以正常工作,而无需使用continue


2
我不确定我是否理解您相对于继续声明的建议。Continue在C和C ++中具有非常不同的含义,如果一种新语言类似于C,但是有时与C一样使用关键字,并且有时以这种替代方式使用该关键字,那么我认为混乱将会盛行。从一个带标签的块落入另一个带标签的块可能存在一些问题,但我喜欢使用共享代码块以相同处理方式处理多个案例。
DeveloperDon

C具有将相同的关键字用于多个目的的悠久传统。想想*&static
idrougge
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.