之前我们介绍了在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. Django的性能优化

    Django的性能优化   一,利用标准数据库优化技术 传统数据库优化技术博大精深,不同的数据库有不同的优化技巧,但重心还是有规则的.在这里算是题外话,挑两点通用的说说: 索引,给关键的字段添加索引, ...

  2. spark 源码分析之二十一 -- Task的执行流程

    引言 在上两篇文章 spark 源码分析之十九 -- DAG的生成和Stage的划分 和 spark 源码分析之二十 -- Stage的提交 中剖析了Spark的DAG的生成,Stage的划分以及St ...

  3. Windows 使用 helm3 和 kubectl

    简介: 主要原因是,我不会 vim ,在 linux 上修改 charts 的很蹩脚,所以就想着能不能再 windows 上执行 helm 命令,将 charts install linux 上搭建的 ...

  4. vmware15pro安装ubuntu18.10时出现显示不全问题

    如果这个时候用网上的ALT+左键拖拽根本没有效果 所以这里提供另外一种方式 就是正常安装的时候发现分区部分显示不全 此时点击右上角的橙色小×:询问是否退出 我们点击退出:之后就会来到试用界面 到了这里 ...

  5. C#连接SQL Anywhere 12 数据库

    using System;using System.Data.Common; namespace ConsoleApplication27{    class Program    {        ...

  6. 你所不知道的 CSS 负值技巧与细节

    写本文的起因是,一天在群里有同学说误打误撞下,使用负的 outline-offset 实现了加号.嗯?好奇的我马上也动手尝试了下,到底是如何使用负的 outline-offset 实现加号呢? 使用负 ...

  7. 【游记】NOIP2018复赛

    声明 我的游记是一个完整的体系,如果没有阅读过往届文章,阅读可能会受到障碍. ~~~上一篇游记的传送门~~~ 前言 参加完NOIP2018的初赛过后,我有点自信心爆棚,并比之前更重视了一点(也仅仅是一 ...

  8. Mysql索引进阶入门

    1. 索引操作 MySQL 索引 菜鸟 2. 索引类型 PRIMARY 唯一且不能为空:一张表只能有一个主键索引 INDEX 普通索引 UNIQUE 唯一性索引 FULLTEXT 全文索引:用于搜索很 ...

  9. 一起来学JavaScript吧(JS兔子领进门)

    首先我们学习一门语言呢不一要学习它的所有历史,但是一定要知道它的使用基本规则.不要在最基础的部分出错.不过胡萝贝还是带你了解JavaScript的历史吧. 1994年网景公司(Netscape)发布了 ...

  10. Zookeeper的命令行操作(三)

    Zookeeper的命令行操作 1. ZooKeeper服务命令 在准备好相应的配置之后,可以直接通过zkServer.sh 这个脚本进行服务的相关操作 1. 启动ZK服务: sh bin/zkSer ...