之前我们介绍了在switch语句中使用整数类型和枚举类型的情况。这一部分继续介绍使用string类型的情况。string类型是switch语句接受的唯一一种引用类型参数。

下面来看一段C#代码。

代码1 - 使用string类型参数的switch语句

代码1展示的方法中只有一个switch语句,它接收一个字符串类型的参数s,并根据6种不同的情况显示不同的文字。它将被编译器翻译成什么样子的代码呢?这个switch语句是否依然能利用IL中的switch指令呢?

答案马上揭晓。且看由代码1得到的IL,如代码2所示。

代码2 - 代码1得到的IL代码

呵呵,第一感觉就是,没见到switch指令。下面我们来简要地分析一下这些代码。

首先是IL_0000到IL_0002,这里还是首先将参数复制到了一个局部变量中,在前面介绍使用整数和枚举的情况时,我们也看到了类似的情况。老刘对此的猜测是,这是由于IL中没有修改方法参数的指令(如starg),而C#语言支持在方法体内给参数赋值——虽然这个值的改动不会影响到调用方法时传递进来的实参。因此,为了满足C#语言的这种特点,编译器生成了一个局部变量,并在方法一开始将参数值复制进来,之后便可以操作这个参数了(修改其值)。再次重申,这是老刘自己的猜测。

如果上述猜测成立的话,那么C#编译器实际上还可以做一些改进,即判断如果方法体内没有修改参数值,则可以省去这个局部变量。

接下来的IL_0003一行是一个条件跳转,ILDasm给出的指令是brfalse,其实写brnull更合适。brfalse和brnull还有brzero指令是一组同义词(他们底层的指令代码是一样的)。这组指令的作用是,从栈顶取出一个元素,判断其值是否为0(值类型所有字段全零、引用类型是null),如果是的话,则跳转到指令参数所指定的语句去执行,否则继续执行下一条指令。

很明显,如果参数s是null的话,该指令将导致执行流程直接跳转到表示case null的指令块中。

接下来,从IL_0005到IL_0037,每四条指令为一组,分别比较了s和四个不同的字符串的相等性,如果与某一个值相等,则跳转到对应的地址,该地址就是这个字符串常量对应的case子句。字符串的相等性是通过op_Equality方法进行的,这相当于使用“==”运算符判断字符串是否相等。

每个指令块(case子句)执行完毕之后,都会有一行br.s IL_0071,这个IL_0071对应的就是switch语句之后的其他语句。

由此可见,对于代码1所示的C#程序片段,编译器实际上是将switch语句翻译成了相当于一串if语句的形式。那么,如此一来,当case子句过多时,岂不是会导致程序变慢?

下面再来看一段代码,我们在switch中放入更多的case子句,请参见代码3。

代码3 - 拥有更多case子句的switch语句

哈哈,老刘不厚道啊,不就多了一个case "five"子句么。

是的,就多这一个。下面我们来看一下代码3对应的IL代码。

代码4 - 代码3对应的IL代码

耶?有奇怪的东西出现。你是不是第一眼也看到了IL_0007这一条指令了?别忙,我们一点一点地拆解它。

首先,这条指令是ldsfld——加载静态字段。然后给出了字段的类型,是class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>类型,这是一个已经实例化的字典泛型类。然后就是具体要加载的字段了,其形式应该为“ClassName::FieldName”;因此可以看出,这个字段所属的类型是<PrivateImplementationDetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA},字段的名字是$$method0x6000007-1。

注解

在ILAsm语言中,#、$、@、_(下划线)和`(注意不是单引号,而是和波浪线“~”位于同一键位上的撇字符)都是标识符中的合法字符。另外,ILAsm还支持用单引号将标识符包围起来,这样甚至还可以在其中使用一些非法字符。

用ILDasm以图形化界面打开生成的程序集,果然可以看到这样一个类型和他的这个字段,如图1所示。

图1 - 编译器为switch语句生成的内部类型

在继续进行之前,老刘再来给大家做一个猜测——这个类型的名字和字段的名字是咋来的。

首先是类型的名字,名字中的尖括号和花括号主要是用来防止与用户编写的标识符发生冲突,因为在绝大多数高级语言中,尖括号和花括号都不能用在标识符中。尖括号中的“PrivateImplementationDetails”明确指出了这个类型是编译器内部实现的,是不属于用户的。尖括号后面,一对花括号之间很明显是一个GUID,观察一下就会发现,这个GUID就是当前模块的MVID(双击“M A N I F E S T”节点可以看到mvid)。

接下来是字段的名字,前导的两个$也是防止命名冲突的。之后的method0x6000007,表示这是给元数据标识为“0x6000007”的方法使用的。最后的“-1”表示这个字段是这个方法中用到的第一个内部实现的结构。

好了,现在我们知道了,编译器自动为我们生成了一个类型,并在其中提供了一个字典类的静态字段。接下来,我们详细看一下发生了什么。

首先IL_0000至IL_0003这几条指令和代码2中的一样,此处不再赘述。IL_0005一行是一个前缀指令volatile.,表明它后面的ldsfld指令要加载的字段是一个“易变”字段,也就是说这个字段可能会被其他进程改变。这就告诉了运行时环境,在访问字段时不要缓存它的值。

注解

在IL指令中,前缀指令只修饰紧随其后的一条指令,其他指令不受影响。

IL_0007和IL_000C两行判断之前提到的那个“内部字段”是否为null,如果不是null则跳转到IL_0057,否则继续执行下面的指令,建立一个新的Dictionary<string,int32>类型的字段。同样,这里的brtrue写作brinst更为合适(brtrue和brinst也是一组同义词,其指令代码是一样的)。

接下来的IL_000e到IL_0052,先是初始化了一个字典类对象,然后分别将case子句中出现的五个字符串(null除外)作为key插入到了这个字典中,每个字符串对应一个整数,从0到4。最后将这个对象保存在“内部字段”中。

接下来走到了IL_0057,也就是之前判断“内部字段”不为空时跳转到的位置。从IL_0057到IL_0061是通过调用字典类的TryGetValue方法尝试从字典中找到key的值是switch参数s所指定的项。

从这里,我们可以看到,在IL中调用方法时,参数是自左向右依次压入堆栈的;如果调用的是实例方法,则在哪个对象上调用方法,应该最先将这个对象压入堆栈。例如在这里,首先压入了“内部字段”,然后是第0个局部变量(复制进来的参数s),最后是第1个参数的地址。

此外,我们还看到了C#中out参数是如何实现的。对于方法声明,out参数会被声明为类似“Type&”这样的类型,这是一个托管指针。在传值时,通过ldloca指令可以得到局部变量的地址。

IL_0066,如果上述TryGetValue方法没有找到对应的key,则跳转到IL_00b8——从这一行的内容来看——是default子句的位置。

IL_0069,啊哈,看到了我们所熟悉的switch指令。根据刚刚取到的整数值,跳转到个个case子句中去运行。

再看switch指令之后的IL_0082位置上的指令,这是一个无条件跳转,直接跳到IL_00b8——default子句。回顾一下switch指令的用法,当从栈顶取到的整数值比switch指令中的跳转地址数量要大时,会忽略switch指令,直接执行接下来的指令。所以,可以认为switch指令后面紧随的指令应该类似于C#语言中switch语句中的default子句。但在这里,编译器按照习惯,将default子句对应的IL代码放到了最后,并在switch指令之后紧接着放置一个无条件跳转,跳转到default子句中。

至此,这段代码基本就分析完了。

小结

本文介绍了在switch语句中使用字符串对象作为参数的情形。

可以看到,当case子句数量不多时,编译器会将其翻译为类似于一系列if语句这样的结构,并通过“==”运算符来与每种case进行比较。

当case子句的数量较多时,编译器则会生成一个内部类,并提供一个字典字段。这个字典字段的key是字符串类型,value是整数类型;其中key记录了每种case,而value记录了对应case子句的序号。之后,以switch语句的参数s作为key,取出对应的value,再利用switch指令做跳转。

这样做是利用了Dictionary<TKey,TValue>类型通过key来取值的时间复杂度接近于O(1)这种特性(请参见MSDN上关于Dictionary泛型类的说明),有助于提高效率。此外,这个字段在需要的时候才进行初始化,并且只初始化一次,进一步提高了程序的整体效率。

如果你的程序中用了大量if语句来判断一个字符串对象是否具有给定的值,不妨将其改为用switch语句实现。如果你有其他引用类型对象,要进行类似的判断,又不能使用switch语句(C#语法不允许),可以尝试自己写一个字典类的字段,以给定的几种可能的对象做key,以连续的整数值作为value,然后每次判断时,通过以给定对象(参数)作为key,取到vlaue后再用switch进行判断。

switch语句(下)(转载)的更多相关文章

  1. switch语句下的变量声明和定义

    switch语句下的变量声明和定义的问题: switch...case...语句中存在声明和定义会出现一些问题.这个由switch语法特性决定的, switch中每个case都是平等的层次,区别于一般 ...

  2. SCXcodeSwitchExpander自动填充switch语句下枚举类型case

    下载地址:https://github.com/stefanceriu/SCXcodeSwitchExpander 跟VVDocumenter规范注释生成器的安装方式一样: 下载开源工程在Xcode重 ...

  3. switch语句(上)(转载)

    switch语句是C#中常用的跳转语句,可以根据一个参数的不同取值执行不同的代码.switch语句可以具备多个分支,也就是说,根据参数的N种取值,可以跳转到N个代码段去运行.这不同于if语句,一条单独 ...

  4. goto语句 switch语句

    goto语句 #include <iostream> using namespace std; int main() { int i = 1; number: i++; std::cout ...

  5. 利用switch语句计算特定的年份的月份共有几天。

    //利用switch语句计算特定的年份的月份共有几天. let year =2015 let month =2 //先判断闰年中二月份的情况 ifmonth ==2 { if (year %400 = ...

  6. java基础2 判断语句:if ... else 语句和 switch 语句

    一.if ... else 判断语句 1.if ... else 判断语句的格式 1.1.格式一 if(判断条件){ 执行不满足条件的语句 } 1.2.格式二 if(判断语句){ 满足条件的语句 }e ...

  7. 多路开关模式的switch语句

    在实例10中,将break语句去掉之后,会将符合检验条件后的所有语句都输出.利用这个特点,可以设计多路开关模式的switch语句,例如:在平年一年12个月,1.3.5.7.8.10.12月是31天,4 ...

  8. switch语句的妙用

    switch语句的普通用法很简单,如下: var a = 3; switch (a) { case 1: console.log(a); break; case 2: case 3: console. ...

  9. 通过goto语句学习if...else、switch语句并简单优化

    goto语句在C语言中实现的就是无条件跳转,第二章一上来就介绍goto语句就是要通过goto语句来更加清楚直观的了解控制结构. 我理解的goto语句其实跟switch语句有相似之处,都是进行跳转.不同 ...

随机推荐

  1. Python学习6——再谈抽象(面对对象编程)

    1.对象魔法 在面对对象编程中,术语对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法. 使用对象而非全局变量以及函数的原因有多个,而最重要的好处不过以下几点: 多态:可对不同类型的对象 ...

  2. CSRF_TOKEN

    目的是防御CSRF攻击. Token就是令牌,最大的特点就是随机性,不可预测. CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中, ...

  3. 深入理解HashMap(jdk7)

    HashMap的结构图示 ​ jdk1.7的HashMap采用数组+单链表实现,尽管定义了hash函数来避免冲突,但因为数组长度有限,还是会出现两个不同的Key经过计算后在数组中的位置一样,1.7版本 ...

  4. ld: warning: directory not found for option ''

    iOS开发中经常遇到这样的警告,如图所示: 原因是存在未用到的目录. 解决方法:选择Build Settings,找到Search Paths中的Library Search Paths,如下图 删除 ...

  5. java-web调用后台下载方法

    后台下载指定文件必定会用到流, 无论使用poi还是使用jxl导出excel都需要用到流一种是outputstrean,另一种fileoutputstream第一种:如果想要弹出保存的提示框必须加入下列 ...

  6. 【Java例题】1.3给朋友的贺卡

    3.对“Hello World”程序进行改造, 能够显示一张发给朋友的贺卡.格式如下: ****************************** 张三,你好! 祝你学习愉快! 你的好朋友:李四 2 ...

  7. collection介绍

    1.collection介绍 在mongodb中,collection相当于关系型数据库的表,但并不需提前创建,更不需要预先定义字段 db.collect1.save({username:'mayj' ...

  8. win7-BIOS中开启AHCI模式电脑蓝屏怎么办?

    win7-BIOS中开启AHCI模式电脑蓝屏怎么办? 来源:U大师 u盘装系统 不少网友都表示给电脑安装win7系统后,如果在BIOS中开启IDE模式就一切正常而为AHCI模式时就会出现蓝屏.其实那是 ...

  9. 关于Linux的简单介绍

    Linux: 诞生日期:1991年 开发者:林纳斯·托瓦茨 特点:免费,开源    发行版本:centos|red Hat|Ubuntu|红旗等    思想:一切都是文件 重要文件目录 bin:二进制 ...

  10. 重读《学习JavaScript数据结构与算法-第三版》- 第3章 数组(一)

    定场诗 大将生来胆气豪,腰横秋水雁翎刀. 风吹鼍鼓山河动,电闪旌旗日月高. 天上麒麟原有种,穴中蝼蚁岂能逃. 太平待诏归来日,朕与先生解战袍. 此处应该有掌声... 前言 读<学习JavaScr ...