C#中的switch语句失败?

Posted

技术标签:

【中文标题】C#中的switch语句失败?【英文标题】:Switch statement fallthrough in C#? 【发布时间】:2010-09-15 12:09:58 【问题描述】:

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:”)转移到另一个案例标签(“案例 3:”) 控制不能从一个案例标签(“案例 2:”)转移到另一个案例标签

为什么?有没有办法在没有三个ifs 的情况下获得这种行为?

【问题讨论】:

【参考方案1】:

(复制/粘贴answer I provided elsewhere)

通过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;

【讨论】:

我认为,在这种特殊情况下,goto 不会被认为是有害的。 创建一个新的保留字fallthrough 会不会有点过头了,你可以用它来表示一个明确的允许失败的愿望。然后编译器可以检查是否意外掉线,但允许有目的的使用。 @Dancrumb:在编写该功能时,C# 尚未添加任何“软”关键字(如“yield”、“var”、“from”和“select”),所以他们有三个真正的选择:1)让“fallthrough”成为一个硬关键字(你不能将它用作变量名),2)编写支持这种软关键字所必需的代码,3)使用已经保留的关键字。 #1 对于那些移植代码来说是个大问题;据我了解,#2 是一项相当大的工程任务;以及他们选择的选项,#3 有一个附带好处:其他开发人员在事后阅读代码可以从 goto 的基本概念中了解该功能 @Jeff Dege - 如果你要打开一个字符串,你的 goto 声明 case("XYZ"): 应该写成 goto case ("XYZ"); 所有这些都在谈论新的/特殊的关键字来明确的失败。他们不能只使用“继续”关键字吗?换一种说法。跳出开关或继续下一个案例(失败)。【参考方案2】:

“为什么”是为了避免意外跌倒,对此我很感激。这是 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 太近了 - 可以在范围等方面进行一些有用的更改。可以说,可以进行模式匹配等的更智能的开关会有所帮助,但这确实从开关变为“检查一系列条件” - 此时可能需要一个不同的名称。

【讨论】:

这就是我心目中switch和if/elseif的区别。 Switch 用于检查单个变量的各种状态,而 if/elseif 可用于检查连接的任意数量的事物,但不一定是单个变量或同一个变量。 如果是为了防止意外跌落,那么我觉得编译器警告会更好。就像你有一个 if 语句有一个赋值:if (result = true) @TalEven-Tov:编译器警告确实应该针对您几乎总是可以将代码修复得更好的情况。就我个人而言,我更喜欢隐式中断,所以一开始不会有问题,但那是另一回事。 令人讨厌的是,他们仍然让你以break 结束案件。 新版本的 C# 幸运地添加了模式匹配 switch 语句。【参考方案3】:

为了补充这里的答案,我认为值得考虑与此相关的相反问题,即。为什么 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 的使用相对清晰,即使没有对此效果的明确注释。

【讨论】:

【参考方案4】:

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

switch(value)

    case 1:// this is still legal
    case 2:

【讨论】:

我永远不明白为什么这不是“案例 1、2:” @David Pfeffer:是的,case 1, 2: 在允许这样做的语言中也是如此。我永远无法理解的是为什么任何现代语言都不会选择允许这样做。 @BCS 带有 goto 语句,多个逗号分隔的选项可能难以处理? @pengut:说case 1, 2: 是一个标签但有多个名称可能更准确。 - FWIW,我认为大多数禁止失败的语言不会将“连续大小写标签”铟的特殊情况处理,而是将大小写标签视为下一条语句的注释,并要求在标记为(一个或更多)案例标签是一个跳跃。 @BCS in C 和其他类似 C 的语言(不确定 C#)逗号 (",") 是一个运算符,因此仅针对这种“情况”重新定义其行为并不是一个好主意.【参考方案5】:

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

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

【讨论】:

【参考方案6】:

他们在设计上忽略了这种行为,以避免当它未被意志使用但导致问题时。

只有在case部分没有语句的情况下才可以使用,比如:

switch (whatever)

    case 1:
    case 2:
    case 3: boo; break;

【讨论】:

【参考方案7】:

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

【讨论】:

C#支持goto,但不支持fallthrough?哇。不仅仅是那些。 C# 是我所知道的唯一具有这种行为方式的语言。 一开始我并不完全喜欢它,但“fall-thru”确实是灾难的根源(尤其是在初级程序员中。)正如许多人指出的那样,C# 仍然允许 fall-thru空行(大多数情况下)。“Kenny”发布了一个链接,突出显示优雅的 Goto 与 switch-case 一起使用。 在我看来这没什么大不了的。 99% 的时间我都不想跌倒,而且我过去曾被虫子烧过。 “这对我来说听起来不是一个好的解决方案”——很抱歉听到你这么说,因为这就是 goto case 的用途。它优于fallthrough的优点是它是明确的。这里有些人反对goto case只是表明他们被灌输了对“goto”的灌输,对这个问题没有任何了解,无法独立思考。当 Dijkstra 写“GOTO 被认为是有害的”时,他正在处理没有任何其他方法来改变控制流的语言。 @JimBalter 然后有多少引用 Dijkstra 的人会引用 Knuth 的话“过早的优化是万恶之源”,尽管这句话是在 Knuth 明确写下goto 有多么有用的时候是在优化代码的时候?【参考方案8】:

break 之类的跳转语句是 在每个案例块之后需要, 包括最后一个块是否是 案例陈述或默认 陈述。除了一个例外,(不像 C++ switch 语句),C# 没有 支持从 一个案例标签到另一个。唯一的那个 例外是如果 case 语句有 没有代码。

-- C# switch() documentation

【讨论】:

我意识到这种行为已记录在案,我想知道为什么会这样,以及获得旧行为的任何替代方法。【参考方案9】:

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

【讨论】:

要是有人在两年前发帖就好了! @Poldie 第一次很有趣... Shilpa 您不需要为每个案例休息或转到,只需针对每个案例都有自己的代码。您可以拥有多个共享代码的案例。【参考方案10】:

你可以通过goto关键字实现像c++一样的fall through。

前:

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;

【讨论】:

要是有人在两年前发帖就好了!【参考方案11】:

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

【讨论】:

【参考方案12】:

switch (C# Reference) says

C# 需要 switch 部分的结尾,包括最后一个,

所以你还需要在你的default部分添加一个break;,否则仍然会出现编译错误。

【讨论】:

谢谢,这对我有帮助;) 语言做出设计决定以避免编程陷阱是一回事,但你不能诚实地说,允许最终声明的失败会以任何方式有害。跨度> 【参考方案13】:

您忘记添加“break;”在案例 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");

【讨论】:

这会产生非常错误的输出。我有意遗漏了 switch 语句。问题是,当几乎没有其他语言有此限制时,为什么 C# 编译器会将此视为错误。 多么令人震惊的无法理解。而你已经有 5 年的时间来删除它,但仍然没有这样做?令人难以置信。

以上是关于C#中的switch语句失败?的主要内容,如果未能解决你的问题,请参考以下文章

C# 简化 switch 语句

C# 语句 分支语句 switch----case----.

在 Switch 中的 Case 语句中添加附加条件

带/不带大括号的 C# Switch 语句....有啥区别?

C#中一种替换switch语句更优雅的写法

优化:switch 语句中的 case 顺序重要吗? [关闭]