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()语句失败,'期望的整数types的值'和case语句失败'期望值为常数'。

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

这是我原来的post,这引发了一些辩论… 因为它是错误的

switch语句与if-else语句不同。 每个案件必须是独一无二的,并静态评估。 不pipe你有多less个case,switch语句都会执行一个常量时间分支。 if-else语句评估每个条件,直到find一个为真的条件。


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

在某些情况下,编译器将使用CIL switch语句,这个语句实际上是一个使用跳转表的常量时间分支。 然而,在Ivan Hamilton所指出的稀less情况下,编译器可能完全生成其他东西。

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

不要把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个使用的插槽)

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

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

使用包含相邻项目的块的expression式集合,编译器可以进行二叉树search,最后是CIL开关。

这充满了“mays”和“mights”,它依赖于编译器(可能与单声道或转子不同)。

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

执行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

然后我也使用了不相邻的caseexpression式:

执行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 [])“附近编译expression式太长或者很复杂

这里有趣的是,二叉树search出现一点(可能不是统计)比CIL开关指令更快。

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

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

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

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


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

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

  jmp ds:300025F0[eax*4] 

如果二叉树search满了:

  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节):

  • 开关标签的值是恒定的
  • 开关标签的值是不同的 (因此,对于给定的开关expression式,只能select一个开关块)

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

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

代码会做什么? 如果案件陈述被重新sorting呢? 事实上,C#使交换机违规的原因之一是交换机的声明可以任意重新排列。

这些规则是有原因的 – 所以程序员可以通过查看一个case块来确定input块的确切条件。 当前面提到的switch语句增长到100行或者更多时,这样的知识是无价的。

大多数情况下,这些限制是由于语言devise者的原因。 潜在的理由可能是与语言历史,理想或编译器devise的简化兼容。

编译器可能(并且确实)select:

  • 创build一个大的if-else语句
  • 使用MSIL开关指令(跳转表)
  • 构build一个Generic.Dictionary <string,int32>,在首次使用时填充它,然后调用Generic.Dictionary <> :: TryGetValue()作为索引传递给MSIL开关指令(跳转表)
  • 使用if-elses和MSIL“开关”跳转的组合

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

为了处理String的情况,编译器最终(在某个时候)使用a.Equals(b)(也可能是a.GetHashCode())。 我认为编译器使用满足这些约束条件的任何对象都是一件麻烦事。

至于需要静态expression式…如果caseexpression式不是确定性的,那么这些优化(散列,caching等)中的一些将不可用。 但是我们已经看到,有时候编译器只是简单地selectif-else-if-else之类的道路。

编辑: lomaxx – 您对“typeof”运算符的理解不正确。 “typeof”运算符用于获取types的System.Type对象(与其超types或接口无关)。 检查具有给定types的对象的运行时兼容性是“is”运算符的作业。 这里使用“typeof”来expression一个对象是无关紧要的。

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

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

诚然,它并不需要 ,许多语言实际上使用dynamic切换语句。 这意味着重新排列“case”子句可以改变代码的行为。

这里有一些有关devise决策背后的有趣信息: 为什么C#switch语句devise为不允许通过,但仍然需要rest?

允许dynamic的expression式可能导致怪物,比如这个PHP代码:

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

坦率地说应该只使用if-else语句。

关于这个话题,杰夫·阿特伍德(Jeff Atwood)表示, 转换语句是一种编程的暴行 。 谨慎使用它们。

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

 var table = new Dictionary<Type, string>() { { typeof(int), "it's an int!" } { typeof(string), "it's a string!" } }; Type someType = typeof(int); Console.WriteLine(table[someType]); 

微软终于听到了你!

现在使用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("<unknown shape>"); break; case null: throw new ArgumentNullException(nameof(shape)); } 

这不是一个原因,但是C#规范第8.7.2节陈述如下:

switch语句的控制types由switchexpression式build立。 如果switchexpression式的types是sbyte,byte,short,ushort,int,uint,long,ulong,char,string或enum-type,那么这就是switch语句的控制types。 否则,从开关expression式的types到以下可能的控制types之一必须存在一个用户定义的隐式转换(第6.4节):sbyte,byte,short,ushort,int,uint,long,ulong,char,string 。 如果不存在这样的隐式转换,或者存在多于一个这样的隐式转换,则会发生编译时错误。

C#3.0规范位于: http : //download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc

犹大的回答给了我一个主意。 您可以使用Dictionary<Type, Func<T>来“伪造”OP的开关行为:

 Dictionary<Type, Func<object, string, string>> typeTable = new Dictionary<Type, Func<object, string, string>>(); typeTable.Add(typeof(int), (o, s) => { return string.Format("{0}: {1}", s, o.ToString()); }); 

这允许您将行为与switch语句的风格相关联。 我相信在编译为IL时,它具有被键入的附加好处,而不是开关式跳转表。

我想没有根本的原因,为什么编译器不能自动翻译你的switch语句:

 if (t == typeof(int)) { ... } elseif (t == typeof(string)) { ... } ... 

但是没有太多的收获。

整数types的case语句允许编译器进行一些优化:

  1. 没有重复(除非您复制编译器检测到的大小写标签)。 在你的例子中,由于inheritance,t可以匹配多个types。 第一场比赛应该执行吗? 他们全部?

  2. 编译器可以select通过跳转表在整数types上实现一个switch语句,以避免所有的比较。 如果你打开一个枚举整数值为0到100,那么它将创build一个包含100个指针的数组,每个switch语句一个。 在运行时,它只是根据打开的整数值从数组中查找地址。 与执行100次比较相比,这可以提供更好的运行时性能。

根据switch语句文档,如果有明确的方式将对象隐式转换为整型,那么它将被允许。 我认为你正在期待一个行为,其中每个case语句将被replace为if (t == typeof(int)) ,但是当你重载那个操作符的时候,这将会打开一整jar蠕虫。 如果您错误地编写了==覆盖,则switch语句的实现细节发生更改时,行为将发生变化。 通过减less整数types和string的比较和那些可以减less到整数types(和旨在)的事情,他们避免了潜在的问题。

中写道:

“无论你有多less个案子,switch语句都会执行一个不变的时间分支。”

由于语言允许在switch语句中使用stringtypes,我假定编译器无法为此types的常量时间分支实现生成代码,并需要生成if-then样式。

@mweerden – 我明白了。 谢谢。

我在C#和.NET方面没有太多的经验,但似乎语言devise师不允许静态访问types系统,除非在狭隘的情况下。 typeof关键字返回一个对象,所以这只能在运行时访问。

我认为Henk用“不可能访问types系统”的东西来钉住它

另一个select是没有命令来键入数字和string可以在哪里。 因此,一个types转换将不能build立一个二叉search树,只是一个线性search。

我同意这个评论 ,使用表驱动的方法通常更好。

在C#1.0中这是不可能的,因为它没有generics和匿名代表。 C#的新版本有脚手架来完成这个工作。 有对象文字的符号也是有帮助的。

我几乎没有C#的知识,但是我怀疑任何一个开关都只是在其他语言中发生,而没有考虑使它更通用,或者开发人员决定扩展它不值得。

严格地说,你是完全正确的,没有理由把这些限制。 有人可能会怀疑,原因是对于允许的情况,实现是非常有效的(正如Brian Ensink( 44921 )所build议的),但是我怀疑如果我使用整数和一些随机的情况,实现是非常有效的(对于if语句) (例如345,-4574和1234203)。 在任何情况下,允许所有事情(或者至less更多的事情)有什么害处,并且说它只对特定情况(例如(几乎)连续的数字)有效。

但是,我可以想象,由于诸如lomaxx( 44918 )给出的原因,人们可能想排除types。

编辑:@亨克( 44970 ):如果string是最大的共享,具有相同的内容的string将是指向同一内存位置以及。 然后,如果可以确保在这些情况下使用的string被连续地存储在内存中,则可以非常有效地实现该开关(即,以2比较的顺序执行,添加和两个跳转)。