C#switch语句限制

在编写switch语句时,在case语句中可以打开什么看起来有两个限制。

例如(是的,我知道,如果你正在做这种事情,这可能意味着你的面向对象(OO)体系结构是有限的 - 这只是一个人为的例子!),

  Type t = typeof(int);

  switch (t) {

    case typeof(int):
      Console.WriteLine("int!");
      break;

    case typeof(string):
      Console.WriteLine("string!");
      break;

    default:
      Console.WriteLine("unknown!");
      break;
  }

这里switch()语句失败,'期望的整数类型的值'和case语句失败'期望值为常数'。

为什么这些限制已经到位,以及根本的理由是什么? 我没有看到为什么switch语句只能屈服于静态分析的任何理由,以及为什么要打开的值必须是整数(即原始的)。 什么是理由?


这是我原来的帖子,引发了一些争论...... 因为它是错误的

switch语句与if-else语句不同。 每个案例必须是唯一的,并进行静态评估。 无论您拥有多少个案例,switch语句都会执行一个常量时间分支。 if-else语句评估每个条件,直到找到一个为真的条件。


事实上,C#switch语句并不总是一个常量时间分支。

在某些情况下,编译器会使用CIL switch语句,这个语句实际上是一个使用跳转表的常量时间分支。 然而,在伊万汉密尔顿所指出的稀少情况下,编译器可能会完全生成其他内容。

这实际上很容易通过编写各种C#开关语句进行验证,一些稀疏,一些密集,并使用ildasm.exe工具查看生成的CIL。


不要将C#switch语句与CIL switch指令混淆。

CIL开关是一个跳转表,它需要将索引编入一组跳转地址。

这只在C#开关的情况相邻时才有用:

case 3: blah; break;
case 4: blah; break;
case 5: blah; break;

但如果不是,它们几乎没有用处:

case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;

(你需要一张〜3000个大小的表格,只使用3个插槽)

使用非相邻表达式,编译器可能会开始执行线性if-else-if-else检查。

对于较大的非相邻表达式集合,编译器可以从二叉树搜索开始,最后if-else-if-else是最后几个项目。

使用包含相邻项目块的表达式集时,编译器可以进行二叉树搜索,最后是CIL开关。

这充满了“mays”和“mights”,它依赖于编译器(可能与Mono或Rotor不同)。

我使用相邻的案例在我的机器上复制了结果:

执行10路开关的总时间,10000次迭代(ms):25.1383
每10路开关的近似时间(ms):0.00251383

执行50路开关的总时间,10000次迭代(ms):26.593
每50路开关的近似时间(ms):0.0026593

执行5000路开关的总时间,10000次迭代(ms):23.7094
每5000次开关的近似时间(ms):0.00237094

执行50000路开关的总时间,10000次迭代(ms):20.0933
每50000路开关的近似时间(ms):0.00200933

然后我也使用了非相邻的case表达式:

执行10路开关的总时间,10000次迭代(ms):19.6189
每10路开关的近似时间(ms):0.00196189

执行500路开关的总时间,10000次迭代(ms):19.1664
每500次开关的近似时间(ms):0.00191664

执行5000路开关的总时间,10000次迭代(ms):19.5871
每5000次开关的近似时间(ms):0.00195871

一个不相邻的50,000个case switch语句不会被编译。
“表达式太长或者很复杂,无法在'ConsoleApplication1.Program.Main(string [])'附近编译”

这里有趣的是,二叉树搜索似乎有点(可能不是统计上)比CIL开关指令更快。

布赖恩,你已经使用了“ 常量 ”这个词,从计算复杂性理论的角度来看,它具有非常明确的含义。 虽然简单的相邻整数示例可能会产生被认为是O(1)(常数)的CIL,但稀疏示例是O(log n)(对数),集群示例介于两者之间,小示例是O(n)(线性)。

这甚至没有解决String情况,在这种情况下可能会创建一个静态的Generic.Dictionary<string,int32> ,并且在第一次使用时将承受一定的开销。 这里的性能将取决于Generic.Dictionary的性能。

如果您检查了C#语言规范(而不是CIL规范),您会发现“15.7.2 switch语句”没有提及“恒定时间”,或者底层实现甚至使用CIL开关指令(假设非常小心这样的事情)。

在一天结束时,在现代系统上针对整数表达式的C#切换是亚微秒操作,通常不值得担心。


当然,这些时间将取决于机器和条件。 我不会关注这些时序测试,我们讨论的微秒持续时间被任何正在运行的“真实”代码所淹没(并且您必须包含一些“真实代码”,否则编译器会优化分支),或者系统抖动。 我的答案基于使用IL DASM来检查由C#编译器创建的CIL。 当然,这不是最终的,因为CPU运行的实际指令是由JIT创建的。

我检查了在我的x86机器上实际执行的最终CPU指令,并且可以确认一个简单的相邻设置开关,如下所示:

  jmp     ds:300025F0[eax*4]

如果二叉树搜索满了:

  cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  …
  cmp     ebx, 0F82h
  jz      30005EEE

想到的第一个原因是历史的

由于大多数C,C ++和Java程序员都不习惯拥有这样的自由,他们并不要求它们。

另一个更有效的原因是语言复杂性会增加

首先,是否应该将对象与.Equals()==运算符进行比较? 两者在某些情况下都有效。 我们应该引入新的语法来做到这一点吗? 我们应该让程序员介绍他们自己的比较方法吗?

另外,允许开启对象会破坏关于switch语句的基本假设 。 控制switch语句有两条规则,即如果允许对象打开,编译器将无法执行(请参阅C#3.0版语言规范,第8.7.2节):

  • 开关标签的值是恒定的
  • 开关标签的值是不同的(因此对于给定的开关表达式只能选择一个开关块)
  • 在假设的情况下考虑这个代码示例,允许非常量事件值:

    void DoIt()
    {
        String foo = "bar";
        Switch(foo, foo);
    }
    
    void Switch(String val1, String val2)
    {
        switch ("bar")
        {
            // The compiler will not know that val1 and val2 are not distinct
            case val1:
                // Is this case block selected?
                break;
            case val2:
                // Or this one?
                break;
            case "bar":
                // Or perhaps this one?
                break;
        }
    }
    

    代码将做什么? 如果案例陈述重新排序会怎样? 事实上,C#使交换机非法通过的原因之一是交换机语句可以任意重新排列。

    这些规则是有原因的 - 所以程序员可以通过查看一个case block来确定block输入的确切条件。 当上述switch语句增长到100行或更多时(它会),这样的知识是无价的。

    链接地址: http://www.djcxy.com/p/84405.html

    上一篇: C# switch statement limitations

    下一篇: How can a variable be used when its definition is bypassed?