C#中的switch语句失败?


365

switch语句的下通是为爱我个人的一个重要原因switchif/else if结构。这里有一个示例:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

聪明人之所以屈服,是因为string[]s应该在函数外部声明:好的,这只是一个例子。

编译器失败,并出现以下错误:

控件不能从一个案例标签(“案例3:”)落入另一个案例标签
控件不能从一个案例标签(“案例2:”)落入另一个案例标签

为什么?还有没有三秒就可以得到这种行为的任何方法if吗?

Answers:


648

(复制/粘贴我在其他地方提供答案

通过下降switch- caseS可通过不具有代码在一个来实现case(见case 0),或者使用特殊的goto case(见case 1)或goto default(见case 2)形式:

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}

121
我认为,在这种情况下,goto不会被认为是有害的。
Thomas Owens

78
该死的-从1.0的早期开始,我就一直在使用C#进行编程,直到现在我还从未见过。只是展示一下,您每天都会学习新事物。
Erik Forbes,2009年

37
一切都很好,Erik。/ I /知道的唯一原因是我是编译器理论的书呆子,他用放大镜阅读ECMA-334规范。
亚历克斯·莱曼

13
@Dancrumb:在编写该功能时,C#尚未添加任何“软”关键字(例如“ yield”,“ var”,“ from”和“ select”),因此它们具有三个实际选项:1 )将“ fallthrough”设置为硬关键字(您不能将其用作变量名),2)编写支持此类软关键字所必需的代码,3)使用已保留的关键字。对于那些移植代码,#1是一个大问题;据我了解,#2是一项相当大的工程任务。以及他们采用的选择,#3有一个附带的好处:其他开发人员在事后阅读代码可以从goto的基本概念中学习该功能
Alex Lyman

10
所有这些都讨论了针对新的/特殊关键字的明确缺陷。他们难道不能只使用'continue'关键字吗?换一种说法。中断开关或继续进行下一个案例(失败)。
Tal Even-Tov

44

“为什么”是为了避免意外掉线,对此我深表感谢。这是C和Java中错误的常见来源。

解决方法是使用goto,例如

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

在我看来,开关/机壳的总体设计有些不幸。它太接近C了-可能会在范围界定等方面进行一些有用的更改。可以说,可以进行模式匹配等的更智能的开关可能会有所帮助,但这实际上是从开关更改为“检查条件序列” -此时可能需要使用其他名称。


1
在我看来,这就是switch和if / elseif之间的区别。开关用于检查单个变量的各种状态,而if / elseif可用于检查所连接的任何事物,但不一定是单个或相同的变量。
马修·沙利

2
如果要防止意外掉线,那么我觉得编译器警告会更好。就像您有一个if语句有一个赋值一样:if (result = true) { }
Tal Even-Tov

3
@ TalEven-Tov:编译器警告实际上应该是在您几乎总是可以将代码改进为更好的情况下。就我个人而言,我更喜欢隐式中断,因此从一开始就不会有问题,但这是另一回事。
乔恩·斯基特

26

从历史上看,切换失败率是现代软件中错误的主要来源之一。语言设计人员决定强制在案例结束时跳转,除非您直接默认下一个案例而不进行处理。

switch(value)
{
    case 1:// this is still legal
    case 2:
}

23
我完全不明白为什么这不是“案例1:2”
BCS

11
@David Pfeffer:是的,case 1, 2:允许的语言也是如此。我永远不会理解的是,为什么任何现代语言都不会选择允许这样做。
BCS

@BCS与goto语句一起使用,多个逗号分隔的选项可能很难处理?
penguat 2011年

@pengut:说case 1, 2:一个标签但有多个名称可能更准确。-FWIW,我认为大多数禁止使用的语言都不是“连续大小写标签”铟的特殊情况,而是将大小写标签视为下一条语句的注释,并要求在标有((或更多)案例标签是一个跳跃。
BCS

22

要在此处添加答案,我认为值得与此同时考虑相反的问题,即。为什么C首先允许失败?

任何编程语言当然都有两个目标:

  1. 向计算机提供说明。
  2. 记录程序员的意图。

因此,任何编程语言的创建都是如何最好地实现这两个目标之间的平衡。一方面,越容易将其转换为计算机指令(无论是机器码,IL之类的字节码,还是在执行时对指令进行解释),那么越有可能使编译或解释过程变得高效,可靠且输出紧凑。达到极限,这个目标导致我们只用汇编,IL甚至原始操作码编写代码,因为最简单的编译就是根本没有编译。

相反,语言表达的语言越多,表明程序员的意图,而不是为此目的而采取的手段,则在编写和维护过程中程序就越容易理解。

现在,switch始终可以通过将其转换为等效的if-else块链或类似链来进行编译,但是它被设计为允许编译为一种特殊的通用装配模式,在该模式中,一个值将获取一个值,然后从中计算一个偏移量(无论是通过查找表来进行)通过值的完美哈希值或通过对值*的实际算术索引)。值得注意的是,今天,C#编译有时会switch变成等效的if-else,有时会使用基于哈希的跳转方法(对于C,C ++和其他具有类似语法的语言也是如此)。

在这种情况下,允许掉线有两个很好的理由:

  1. 无论如何,它都是自然发生的:如果将跳转表构建到一组指令中,而较早的一批指令中不包含某种跳转或返回,则执行将自然而然地进入下一批。如果将switch使用C的代码转换为使用机器代码的跳转表,则允许失败的发生只是“发生” 。

  2. 用汇编语言编写的编码人员已经习惯了等效的用法:当用手工汇编语言编写跳转表时,他们将不得不考虑给定的代码块是返回返回,表外跳转还是继续执行。到下一个区块。这样,让编码器break在必要时添加显式内容对于编码器来说也是“自然的”。

因此,在当时,这是一种合理的尝试,要在计算机语言的两个目标之间取得平衡,因为它既与所产生的机器代码有关,又与源代码的表现力有关。

但是,四十年后,由于某些原因,情况并不太相同:

  1. 如今,用C编写代码的人员可能很少或没有汇编经验。使用许多其他C风格语言的编码器的可能性更低(尤其是Javascript!)。任何“人们习惯于集会”的概念都不再相关。
  2. 在最佳化改进意味着的可能性switch或者正在变成if-else,因为它被认为是该方法可能是最有效的,否则变成了跳表方式的特别深奥的变异较高。上层和下层方法之间的映射不像以前那样牢固。
  3. 经验表明,掉线往往是少数情况,而不是正常情况(对Sun编译器的研究发现,有3%的switch块使用了掉线,而不是在同一块上使用多个标签,并且认为使用-此处的案例意味着这3%实际上比正常水平要高得多)。因此,所研究的语言比平常更容易迎合不寻常的情况。
  4. 经验表明,在意外完成的情况下以及在维护代码的人错过了正确的失败率的情况下,失败率都是导致问题的根源。后者是与穿透相关的错误的微妙补充,因为即使您的代码完全没有错误,但穿透仍可能导致问题。

与后两点相关,请考虑以下当前K&R版本中的引用:

从一个案例过渡到另一个案例并不稳健,在修改程序时易于分解。除了用于单个计算的多个标签外,应谨慎使用掉线并添加注释。

从形式上讲,即使在逻辑上不必要,在最后一种情况(此处为默认值)之后也要稍事休息。总有一天,当另一种情况被添加到最后时,这种防御性编程将为您节省。

因此,从马的嘴巴中,C掉线是有问题的。始终记录带有注释的失败被认为是一种好习惯,这是一个普遍原则的应用,即应该记录一个人所做的不寻常的事情,因为这会使以后的代码检查和/或使您的代码看起来像这样实际上是正确的时,有一个新手的错误。

当您考虑它时,代码如下:

switch(x)
{
  case 1:
   foo();
   /* FALLTHRU */
  case 2:
    bar();
    break;
}

添加的东西,使落空在代码中明确的,它只是没有东西可以被检测(或者其缺乏可检测)的编译器。

因此,必须在C#中明确显示on的事实并不会给使用其他C风格语言写得很好的人增加任何惩罚,因为他们在错误中已经很明确了。†

最后,在goto这里使用C已经成为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关于我们正在做的事情更清晰的标签。(我们还必须添加breakdefault,这是我离开了只是为了让上面的C代码显然不是C#代码)。

因此,总而言之:

  1. C#不再像40年前的C代码一样直接与未经优化的编译器输出相关(如今C也不是如此),这使得掉线的灵感之一不相关。
  2. C#不仅与C保持兼容,而且具有隐式break,以便熟悉类似语言的人更轻松地学习该语言,并更轻松地进行移植。
  3. C#消除了可能的错误或误解代码的来源,这些错误或误解的代码在过去的40年中一直被证明会引起问题。
  4. C#使编译器可以执行C(文档掉入)的现有最佳实践。
  5. C#使异常情况下的代码更加明确,而通常情况下代码中的代码只是自动编写。
  6. C#使用与C中相同的goto基于方法,从不同的case标签中击中相同的块。只是将其推广到其他一些情况。
  7. goto通过允许case语句充当标签,C#使基于C 的方法比C中的方法更方便,更清晰。

总而言之,一个相当合理的设计决策


*某些形式的BASIC将允许执行GOTO (x AND 7) * 50 + 240类似的操作,而这种操作虽然很脆弱,但因此是一个特别有说服力的禁止案例goto,但确实显示了较高语言,相当于较低级代码可以基于以下方式进行跳转的方式对值进行算术运算,这是编译的结果,而不是必须手动维护的值,这更加合理。Duff设备的实现尤其适合使用等效的机器代码或IL,因为每个指令块通常具有相同的长度,而无需添加nop填充符。

†作为合理的例外,达夫的装置再次出现在这里。通过这种操作和类似的模式,可以重复执行操作,即使没有对此效果进行明确评论,也可以使穿透功能的使用相对清晰。


17

您可以“转到案件标签” http://www.blackwasp.co.uk/CSharpGoto.aspx

goto语句是一个简单的命令,可无条件地将程序的控制权转移到另一个语句。该命令经常受到一些开发人员的批评,他们主张将其从所有高级编程语言中删除,因为它可能导致产生意粉代码。当goto语句或类似的jump语句太多而导致代码难以阅读和维护时,就会发生这种情况。但是,有些程序员指出goto语句在谨慎使用时可以为某些问题提供一个优雅的解决方案...


8

他们通过设计排除了此行为,以避免在遗嘱中不使用它而引起问题。

仅当案例部分中没有语句时才可以使用它,例如:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}

4

他们更改了c#的switch语句(来自C / Java / C ++)的行为。我猜想原因是人们忘记了失败并造成了错误。我读过的一本书说过要使用goto进行模拟,但这听起来对我来说不是一个好的解决方案。


2
C#支持goto,但不支持失败?哇。不只是那些。C#是我所知道的唯一以这种方式运行的语言。
马修·沙利

我一开始并不完全喜欢它,但“直通”确实是灾难的根源(尤其是在初级程序员中)。正如许多人指出的那样,C#仍然允许空行直通(这是大多数案例。)“ Kenny”发布了一个链接,突出显示了Goto在开关盒中的优雅用法。
椒盐脆饼

我认为这不是什么大问题。99%的时间我不想失败,过去我被虫子烧死了。

1
“这听起来对我来说不是一个很好的解决方案” –很遗憾听到您的消息,因为那goto case是出于此目的。它比失败的优势在于它是显式的。这里有些人的反对goto case只是表明他们被灌输反对“ goto”的权利,而对这个问题一无所知,无法独立思考。当Dijkstra撰写“ GOTO被认为有害”时,他所使用的语言没有其他改变控制流程的方法。
Jim Balter

@JimBalter,然后有多少引用Dijkstra的人会引用Knuth的话,“过早的优化是万恶之源”,尽管那句话是Knuth明确写出goto优化代码的有用性的时候?
乔恩·汉纳

1

您可以通过goto关键字像c ++一样实现失败。

例如:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}

9
如果只有两年前有人发布过!

0

在每个case块之后都需要一个跳转语句,例如break,无论是case语句还是默认语句,包括最后一个块。除了一个例外(与C ++ switch语句不同)之外,C#不支持从一个case标签到另一个case标签的隐式掉线。一个例外是case语句没有代码。

- C#开关()文档


1
我意识到这种行为已被记录在案,我想知道为什么会这样,并且有其他选择可以保留旧的行为。
马修·沙利

0

在每个case语句之后,即使是默认情况,也需要breakgoto语句。


2
如果只有两年前有人发布过!

1
@Poldie第一次很有趣... Shilpa,您不需要为每种情况都中断或跳转,只需为每种情况使用自己的代码即可。您可以有多个共享代码的案例。
小牛

0

简要说明一下,Xamarin的编译器实际上出错了,并且允许失败。据称它已修复,但尚未发布。在实际遇到的一些代码中发现了这一点,编译器没有抱怨。



-1

C#不支持switch / case语句失败。不知道为什么,但是实际上没有任何支持。连锁


错了 参见其他答案... 隐式失败的影响可以通过显式的 获得goto case。这是C#设计人员的有意明智的设计选择。
Jim Balter

-11

您忘记添加“中断”;语句进入情况3。在情况2中,您将其写入了if块。因此,请尝试以下操作:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}

2
这会产生非常错误的输出。我故意遗弃了switch语句。问题是,当几乎没有其他语言具有此限制时,为什么C#编译器会将此视为错误。
马修·沙利

5
令人难以理解的失败。而且您已经有5年的时间删除它了,但是还没有删除吗?令人难以置信。
Jim Balter
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.