switch语句是C#中常用的跳转语句,可以根据一个参数的不同取值执行不同的代码。switch语句可以具备多个分支,也就是说,根据参数的N种取值,可以跳转到N个代码段去运行。这不同于if语句,一条单独的if语句只具备两个分支(这是因为if语句的参数只能具备true或false两种取值),除非使用嵌套if语句。

switch语句能够接受的参数是有限制的,简单来说,只能是整数类型、枚举或字符串。本文就从整数、枚举和字符串这三种类型的switch语句进行介绍。

switch指令

在进入正题之前,先为大家简要介绍一下IL汇编语言中的switch指令。switch指令(注意和C#中的switch语句区分开)是IL中的多分支指令,它的基本形式如下:

switch (Label_1, Label_2, Label_3…)

其中switch是IL关键字,Label_1~Label_N是一系列标号(和goto语句中用到的标号一样),标号指明了代码中的位置。这条指令的运行原理是,从运算栈顶弹出一个无符号整数值,如果该值是0,则跳转到由Label_1指定的位置执行;如果是1,则跳转到Labe_2;如果是2,则跳转到Label_3;以此类推。

如果栈顶弹出的值不在标号列表的范围之内(0~N-1),则忽略switch指令,跳到switch指令之后的一条指令开始执行。因此,对于switch指令来说,其 “default子句”是在最开头的。

此外,Label_x所引用的标号位置只要位于当前方法体就可以,不必非要在switch指令的后面。

好了,后面我们会看到switch指令的实例的。

使用整数类型的switch语句

代码1 - 使用整数类型参数的switch语句,取值连续

代码1中的switch语句接受的参数n是int类型的,并且我们观察到,在各个case子句中的取值都是连续的。将这段代码写在一个完整的程序中,并进行编译。之后使用ildasm打开生成的程序集,可以看到对应的IL代码如代码2所示。

代码2 – 代码1生成的IL代码

我们可以看到,首先IL_0000和IL_0001两行代码将参数n存放到一个局部变量中,然后IL_0002到IL_0004三行将这个变量的值减去1,并将结果留在运算栈顶。啊哈,参数值减去1,要进行判断的几种情况不就变成了0、1、2了么?是的。在接下来的switch指令里,针对这三种取值给出了三个地址IL_0017、IL_0022和IL_002d。这三个地址处的代码,分别就是取值为1、2、3时需要执行的代码。

以上是取值连续的情形。如果各个case子句中给出的值并不连续呢?我们来看一下下面的C#代码:

代码3 – 使用整数类型参数的switch语句,取值不连续

代码3编译生成的程序集中,编译器生成的IL代码如下:

代码4 – 代码3生成的IL代码

看到代码4,第一感觉就是switch指令中跳转地址的数量和C#程序中switch语句中的取值数不相符。但仔细观察后可以发现,switch指令中针对0、2、4(即switch语句中的case 1、3、5)这三种取值给出了不同的跳转地址。而对于1、3这两种取值(在switch语句中并没有出现)则给出了同样的地址IL_003f,看一下这个地址,是语句ret。

也就是说,对于取值不连续的情况,编译器会自动用“default子句”的地址来填充switch指令中的“缝隙”。当然,代码4因为过于简单,所以“缝隙值”直接跳转到了方法的结尾。

那么,如果取值更不连续呢?那样的话,switch指令中就会有大量的“缝隙值”。要知道,switch指令和之后的跳转地址列表都是指令的一部分,缝隙值的增加势必会导致程序集体积的增加啊。呵呵,不必担心,编译器很聪明,请看下面的代码:

代码5 – 使用整数类型参数的switch语句,取值非常不连续

在代码5中,switch语句的每个case子句中给出的取值之间都相差20,这意味着如果再采用前面所述“缝隙值”的做法,switch指令中将有多达41个跳转地址,而其中有效的只有3个。但现代的编译器明显不会犯这种低级错误。下面给出编译器为代码5 生成的IL:

代码6 – 代码5生成的IL代码

从代码6中我们会发现,switch指令不见了,在IL_0005、IL_000a和IL_000f三处分别出西安了beq.s指令,这个指令是beq指令的简短形式。当跳转位置和当前位置之差在一个sbyte类型的范围之内时,编译器会自动选择简短形式,目的是缩小指令集的体积。而beq指令的作用是从运算栈中取出两个值进行比较,如果两个值相等,则跳转到目标位置(有beq指令后面的参数指定)执行,否则继续从beq指令的下一条指令开始执行。

由此可见,当switch语句的取值非常不连续时,编译器会放弃使用switch指令,转而用一系列条件跳转来实现。这有点类似于if-else if-...-else语句。

使用枚举类型的switch语句

.NET中的枚举是一种特殊的值类型,它必须以某一种整数类型作为其底层类型(underlying type)。因此在运算时,枚举都是按照整数类型对待的,switch指令会将栈顶的枚举值自动转换成一个无符号整数,然后进行判断。

因此,在switch语句中使用枚举和使用整数类型没有太大的区别。请看下面一段代码:

代码7 - 在switch语句中使用枚举类型

其中的Num类型是一个枚举,定义为public enum Num { One, Two, Three }

下面是编译器为代码7生成的IL代码:

代码8 - 代码7生成的IL代码

可以看到,代码8和代码2没有什么本质区别。这是因为枚举值就是按照整数对待的。并且,如果枚举定义的成员取值不连续,生成的代码也会和代码4、代码6类似。

小结

本文介绍了编译器如何翻译使用整数类型的switch语句。如果你很在乎微乎其微的效率提升的话,应记得:

  • 尽量在switch中使用连续的取值;
  • 如果取值不连续,则使用尽量少的case子句,并将出现频率高的case放在前面(因为此时switch语句和if-else if-else语句是类似的)。

最新文章

  1. nginx+webpy配置
  2. http apr 8080 exec 3解决
  3. BZOJ3118 : Orz the MST
  4. (转)使用Ping获得局域网机器信息
  5. linux 深入检测io详情的工具iopp
  6. 移动端rem布局的适配mixin【转藏】
  7. MySQL 5.6 SQL 优化及 5.6手册
  8. theano安装
  9. JS——基础知识
  10. php 极简框架ES发布(代码总和不到 400 行)
  11. es6函数的rest参数和拓展运算符(...)的解析
  12. es6重点笔记:let,const
  13. Angular 学习笔记 (路由外传 - RouteReuseStrategy)
  14. iOS 利用高德地图WMS服务
  15. CSC 172 (Data Structures and Algorithms)
  16. 你不可不知的Java引用类型之——SoftReference源码详解
  17. faster rcnn 做识别
  18. Codeforces.1110F.Nearest Leaf(线段树)
  19. sqlalchemy 模型中添加根据身份证号计算性别和年龄
  20. 1080 MOOC期终成绩

热门文章

  1. java读写文件小心缓存数组
  2. ASP.NET Core Web Api之JWT VS Session VS Cookie(二)
  3. win10安装.NET Framework 3.5方法
  4. Spring Cloud 相关资料链接
  5. java多线程中wait/notify/sleep/join/yield方法以及多线程的六种状态
  6. Springmvc的运行原理 SpringMvc的优点
  7. 基于 WPF 模块化架构下的本地化设计实践
  8. js及jquery常用代码
  9. Intellij IDEA 2019.2 设置Git
  10. django分页的写法,前端后端!