当前位置:  开发笔记 > 编程语言 > 正文

C#switch语句限制 - 为什么?

如何解决《C#switch语句限制-为什么?》经验,为你挑选了8个好方法。

在编写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语句失败并带有'a expected value is expected'.

为什么会有这些限制,以及基本理由是什么?我看不出有任何理由switch语句具有只能屈从于静态分析,为什么在接通的值必须是完整的(即原语).理由是什么?



1> Ivan Hamilto..:

重要的是不要将C#switch语句与CIL开关指令混淆.

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):
每10路开关25.1383 近似时间(ms):0.00251383

执行50路开关的总时间,10000次迭代(ms):
每50路开关的大约时间为26.593 (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):
每个500路开关的近似时间为19.1664 (ms):0.00191664

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

不相邻的50,000个案例切换语句将无法编译.
"在'ConsoleApplication1.Program.Main(string [])'附近编译表达式太长或太复杂

这里有趣的是,二叉树搜索比CIL切换指令更快(可能不是统计上).

Brian,你使用了" 常量 " 这个词,从计算复杂性理论的角度来看,它具有非常明确的含义.虽然简化的相邻整数示例可以产生被认为是O(1)(常数)的CIL,但稀疏示例是O(log n)(对数),聚类示例介于两者之间,小例子是O(n)(线性) ).

这甚至不能解决Generic.Dictionary可能会创建静态的String情况,并且在首次使用时会遇到明确的开销.这里的表现将取决于表现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



2> Brian Ensink..:

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

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


实际上,C#switch语句并不总是一个恒定时间分支.

在某些情况下,编译器将使用CIL开关语句,该语句确实是使用跳转表的恒定时间分支.然而,在Ivan Hamilton指出的稀疏情况下,编译器可能完全生成其他东西.

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


如其他答案(包括我的)中所述,此答案中的声明不正确.我建议删除(如果只是为了避免强制执行此(可能是常见的)误解).

3> Antti Kissan..:

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

由于大多数C,C++和Java程序员不习惯拥有这样的自由,因此他们并不要求这些自由.

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

首先,是否应将对象.Equals()==运营商进行比较?两者在某些情况下都有效.我们应该引入新的语法吗?我们应该允许程序员介绍他们自己的比较方法吗?

此外,允许打开对象会破坏有关switch语句的基本假设.管理switch语句有两个规则,如果允许打开对象,编译器将无法强制执行(参见C#3.0版语言规范,§8.7.2):

切换标签的值是不变的

switch标签的​​值是不同的(因此只能为给定的switch-expression选择一个switch块)

在假设的情况下考虑这个代码示例,允许非常量的case值:

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#使交换机掉线非法的原因之一是switch语句可以任意重新排列.

这些规则是有原因的 - 因此程序员可以通过查看一个案例块,确切地知道输入块的精确条件.当上述switch语句增长到100行或更多(并且它将会)时,这样的知识是非常宝贵的.


关于开关重新排序的注意事项.如果案件不包含代码,则通过合法是合法的.例如,案例1:案例2:Console.WriteLine("Hi"); 打破;

4> Ivan Hamilto..:

大多数情况下,由于语言设计者的限制,这些限制已经到位 潜在的理由可能是与languange历史,理想或编译器设计的简化兼容.

编译器可以(并且确实)选择:

创建一个大的if-else语句

使用MSIL开关指令(跳转表)

构建一个Generic.Dictionary ,在第一次使用时填充它,并调用Generic.Dictionary <> :: TryGetValue()以获取索引以传递给MSIL开关指令(跳转表)

使用if-elses和MSIL"switch"跳跃的组合

switch语句不是一个恒定的时间分支.编译器可能会发现快捷方式(使用散列桶等),但更复杂的情况会产生更复杂的MSIL代码,有些情况会比其他情况早出现.

为了处理String情况,编译器将使用a.Equals(b)(以及可能的a.GetHashCode())结束(在某些时候).我认为编译器使用满足这些约束的任何对象是很简单的.

至于静态案例表达式的需要......如果案例表达式不确定,那么这些优化中的一些(散列,缓存等)将不可用.但是我们已经看到,有时编译器只选择简单的if-else-if-else路径......

编辑:lomaxx - 您对"typeof"运算符的理解不正确."typeof"运算符用于获取类型的System.Type对象(与其超类型或接口无关).检查具有给定类型的对象的运行时兼容性是"is"操作员的工作.在这里使用"typeof"来表达对象是无关紧要的.



5> Konrad Rudol..:

顺便说一下,具有相同底层架构的VB允许更灵活的Select Case语句(上面的代码可以在VB中工作)并且仍然可以生成有效的代码,因此必须仔细考虑技术约束的参数.



6> Judah Gabrie..:

根据Jeff Atwood 的说法,关于这个主题,switch语句是一种编程暴行.谨慎使用它们.

您通常可以使用表格完成相同的任务.例如:

var table = new Dictionary()
{
   { typeof(int), "it's an int!" }
   { typeof(string), "it's a string!" }
};

Type someType = typeof(int);
Console.WriteLine(table[someType]);


你是否真的在没有证据的情况下引用某人的手铐Twitter帖子?至少链接到一个可靠的来源.
它来自可靠的来源; 有关Twitter的帖子来自Jeff Atwood,他是您正在浏览的网站的作者.:-)如果你很好奇,杰夫有一些关于这个主题的博客文章.

7> Roman Starko..:

我没有看到任何理由为什么switch语句只能用于静态分析

诚然,它不具备对,很多语言都在实际上使用动态switch语句.然而,这意味着重新排序"case"子句可以改变代码的行为.

在这里进行"切换"的设计决策背后有一些有趣的信息:为什么C#switch语句被设计为不允许掉线,但仍需要休息?

允许动态案例表达式可能会导致诸如此PHP代码之类的怪物:

switch (true) {
    case a == 5:
        ...
        break;
    case b == 10:
        ...
        break;
}

坦率地应该使用该if-else声明.



8> dimaaan..:

微软终于听到你了!

现在使用C#7,您可以:

switch(shape)
{
case Circle c:
    WriteLine($"circle with radius {c.Radius}");
    break;
case Rectangle s when (s.Length == s.Height):
    WriteLine($"{s.Length} x {s.Height} square");
    break;
case Rectangle r:
    WriteLine($"{r.Length} x {r.Height} rectangle");
    break;
default:
    WriteLine("");
    break;
case null:
    throw new ArgumentNullException(nameof(shape));
}

推荐阅读
女女的家_747
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有