在编写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语句具有只能屈从于静态分析,为什么在接通的值必须是完整的(即原语).理由是什么?
重要的是不要将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
这是我的原帖,引发了一些争论...... 因为它错了:
switch语句与if-else语句不同.每个案例必须是唯一的并且静态评估.无论您有多少个案例,switch语句都会执行一个恒定时间分支.if-else语句计算每个条件,直到找到一个为真.
实际上,C#switch语句并不总是一个恒定时间分支.
在某些情况下,编译器将使用CIL开关语句,该语句确实是使用跳转表的恒定时间分支.然而,在Ivan Hamilton指出的稀疏情况下,编译器可能完全生成其他东西.
这实际上很容易通过编写各种C#switch语句来验证,一些稀疏,一些密集,并使用ildasm.exe工具查看生成的CIL.
想到的第一个原因是历史:
由于大多数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行或更多(并且它将会)时,这样的知识是非常宝贵的.
大多数情况下,由于语言设计者的限制,这些限制已经到位 潜在的理由可能是与languange历史,理想或编译器设计的简化兼容.
编译器可以(并且确实)选择:
创建一个大的if-else语句
使用MSIL开关指令(跳转表)
构建一个Generic.Dictionary
使用if-elses和MSIL"switch"跳跃的组合
switch语句不是一个恒定的时间分支.编译器可能会发现快捷方式(使用散列桶等),但更复杂的情况会产生更复杂的MSIL代码,有些情况会比其他情况早出现.
为了处理String情况,编译器将使用a.Equals(b)(以及可能的a.GetHashCode())结束(在某些时候).我认为编译器使用满足这些约束的任何对象是很简单的.
至于静态案例表达式的需要......如果案例表达式不确定,那么这些优化中的一些(散列,缓存等)将不可用.但是我们已经看到,有时编译器只选择简单的if-else-if-else路径......
编辑:lomaxx - 您对"typeof"运算符的理解不正确."typeof"运算符用于获取类型的System.Type对象(与其超类型或接口无关).检查具有给定类型的对象的运行时兼容性是"is"操作员的工作.在这里使用"typeof"来表达对象是无关紧要的.
顺便说一下,具有相同底层架构的VB允许更灵活的Select Case
语句(上面的代码可以在VB中工作)并且仍然可以生成有效的代码,因此必须仔细考虑技术约束的参数.
根据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]);
我没有看到任何理由为什么switch语句只能用于静态分析
诚然,它不具备对,很多语言都在实际上使用动态switch语句.然而,这意味着重新排序"case"子句可以改变代码的行为.
在这里进行"切换"的设计决策背后有一些有趣的信息:为什么C#switch语句被设计为不允许掉线,但仍需要休息?
允许动态案例表达式可能会导致诸如此PHP代码之类的怪物:
switch (true) { case a == 5: ... break; case b == 10: ... break; }
坦率地应该使用该if-else
声明.
微软终于听到你了!
现在使用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)); }