透过IL看C# switch语句(上)
摘要: switch语句是 C#中常用的跳转语句,可以根据一个参数的不同取值执行不同的代码。本文介绍了当向 switch语句中传入不同类型的参数时,编译器为其生成的 IL代码。这一部分介绍的是,在 switch语句中使用整数类型和枚举类型的情况。
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语句,取值连续
static void TestSwitchInt(int n) { switch(n) { case 1: Console.WriteLine("One"); break; case 2: Console.WriteLine("Two"); break; case 3: Console.WriteLine("Three"); break; } }
代码 1中的 switch语句接受的参数 n是 int类型的,并且我们观察到,在各个 case子句中的取值都是连续的。将这段代码写在一个完整的程序中,并进行编译。之后使用 ildasm打开生成的程序集,可以看到对应的 IL代码如代码 2所示。
代码 2 –代码 1生成的 IL代码
.method private hidebysig static void TestSwitchInt(int32 n) cil managed { // Code size 56 (0x38) .maxstack 2 .locals init (int32 V_0) IL_0000: ldarg.0 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: ldc.i4.1 IL_0004: sub IL_0005: switch ( IL_0017, IL_0022, IL_002d) IL_0016: ret IL_0017: ldstr "One" IL_001c: call void [mscorlib]System.Console::WriteLine(string) IL_0021: ret IL_0022: ldstr "Two" IL_0027: call void [mscorlib]System.Console::WriteLine(string) IL_002c: ret IL_002d: ldstr "Three" IL_0032: call void [mscorlib]System.Console::WriteLine(string) IL_0037: ret } // end of method Program::TestSwitchInt
我们可以看到,首先 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语句,取值不连续
static void TestSwitchInt2(int n) { switch(n) { case 1: Console.WriteLine("1"); break; case 3: Console.WriteLine("3"); break; case 5: Console.WriteLine("5"); break; } }
代码 3编译生成的程序集中,编译器生成的 IL代码如下:
代码 4 –代码 3生成的 IL代码
.method private hidebysig static void TestSwitchInt2(int32 n) cil managed { // Code size 64 (0x40) .maxstack 2 .locals init (int32 V_0) IL_0000: ldarg.0 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: ldc.i4.1 IL_0004: sub IL_0005: switch ( IL_001f, // 0 IL_003f, // 1 IL_002a, // 2 IL_003f, // 3 IL_0035) // 4 IL_001e: ret IL_001f: ldstr "1" IL_0024: call void [mscorlib]System.Console::WriteLine(string) IL_0029: ret IL_002a: ldstr "3" IL_002f: call void [mscorlib]System.Console::WriteLine(string) IL_0034: ret IL_0035: ldstr "5" IL_003a: call void [mscorlib]System.Console::WriteLine(string) IL_003f: ret } // end of method Program::TestSwitchInt2
看到代码 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语句,取值非常不连续
static void TestSwitchInt3(int n) { switch(n) { case 10: Console.WriteLine("10"); break; case 30: Console.WriteLine("30"); break; case 50: Console.WriteLine("50"); break; } }
在代码 5中,switch语句的每个case子句中给出的取值之间都相差20,这意味着如果再采用前面所述 “缝隙值 ”的做法, switch指令中将有多达41个跳转地址,而其中有效的只有 3个。但现代的编译器明显不会犯这种低级错误。下面给出编译器为代码 5 生成的 IL:
代码 6 –代码 5生成的 IL代码
.method private hidebysig static void TestSwitchInt3(int32 n) cil managed { // Code size 51 (0x33) .maxstack 2 .locals init (int32 V_0) IL_0000: ldarg.0 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: ldc.i4.s 10 IL_0005: beq.s IL_0012 IL_0007: ldloc.0 IL_0008: ldc.i4.s 30 IL_000a: beq.s IL_001d IL_000c: ldloc.0 IL_000d: ldc.i4.s 50 IL_000f: beq.s IL_0028 IL_0011: ret IL_0012: ldstr "10" IL_0017: call void [mscorlib]System.Console::WriteLine(string) IL_001c: ret IL_001d: ldstr "30" IL_0022: call void [mscorlib]System.Console::WriteLine(string) IL_0027: ret IL_0028: ldstr "50" IL_002d: call void [mscorlib]System.Console::WriteLine(string) IL_0032: ret } // end of method Program::TestSwitchInt3
从代码 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语句中使用枚举类型
static void TestSwitchEnum(Num n) { switch(n) { case Num.One: Console.WriteLine("1"); break; case Num.Two: Console.WriteLine("2"); break; case Num.Three: Console.WriteLine("3"); break; } }
其中的 Num类型是一个枚举,定义为 public enum Num { One, Two, Three }
下面是编译器为代码7生成的IL代码:
代码 8 -代码 7生成的 IL代码
.method private hidebysig static void TestSwitchEnum(valuetype AndersLiu.CSharpViaIL.Switch.Num n) cil managed { // Code size 54 (0x36) .maxstack 1 .locals init (valuetype AndersLiu.CSharpViaIL.Switch.Num V_0) IL_0000: ldarg.0 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: switch ( IL_0015, IL_0020, IL_002b) IL_0014: ret IL_0015: ldstr "1" IL_001a: call void [mscorlib]System.Console::WriteLine(string) IL_001f: ret IL_0020: ldstr "2" IL_0025: call void [mscorlib]System.Console::WriteLine(string) IL_002a: ret IL_002b: ldstr "3" IL_0030: call void [mscorlib]System.Console::WriteLine(string) IL_0035: ret } // end of method Program::TestSwitchEnum
可以看到,代码 8和代码 2没有什么本质区别。这是因为枚举值就是按照整数对待的。并且,如果枚举定义的成员取值不连续,生成的代码也会和代码 4、代码 6类似。
小结
本文介绍了编译器如何翻译使用整数类型的 switch语句。如果你很在乎微乎其微的效率提升的话,应记得:
· 尽量在 switch中使用连续的取值;
· 如果取值不连续,则使用尽量少的case子句,并将出现频率高的case放在前面(因为此时switch语句和if-else if-else语句是类似的)。
透过IL看C# switch语句(下)
摘要: switch语句是 C#中常用的跳转语句,可以根据一个参数的不同取值执行不同的代码。本文介绍了当向 switch语句中传入不同类型的参数时,编译器为其生成的 IL代码。这一部分介绍的是,在 switch语句中使用字符串类型的情况。
之前我们介绍了在switch语句中使用整数类型和枚举类型的情况。这一部分继续介绍使用 string类型的情况。string类型是switch语句接受的唯一一种引用类型参数。
下面来看一段 C#代码。
代码 1 -使用 string类型参数的 switch语句
static void TestSwitchString(string s) { switch(s) { case null: Console.WriteLine("<null>"); break; case "one": Console.WriteLine(1); break; case "two": Console.WriteLine(2); break; case "three": Console.WriteLine(3); break; case "four": Console.WriteLine(4); break; default: Console.WriteLine("<Unknown>"); break; } Console.WriteLine("After switch."); }
代码1展示的方法中只有一个 switch语句,它接收一个字符串类型的参数s,并根据6种不同的情况显示不同的文字。它将被编译器翻译成什么样子的代码呢?这个switch语句是否依然能利用 IL中的switch指令呢?
答案马上揭晓。且看由代码 1得到的IL,如代码2所示。
代码 2 -代码 1得到的 IL代码
.method private hidebysig static void TestSwitchString(string s) cil managed { // Code size 124 (0x7c) .maxstack 2 .locals init (string V_0) IL_0000: ldarg.0 IL_0001: dup IL_0002: stloc.0 IL_0003: brfalse.s IL_003b IL_0005: ldloc.0 IL_0006: ldstr "one" IL_000b: call bool [mscorlib]System.String::op_Equality(string, string) IL_0010: brtrue.s IL_0047 IL_0012: ldloc.0 IL_0013: ldstr "two" IL_0018: call bool [mscorlib]System.String::op_Equality(string, string) IL_001d: brtrue.s IL_004f IL_001f: ldloc.0 IL_0020: ldstr "three" IL_0025: call bool [mscorlib]System.String::op_Equality(string, string) IL_002a: brtrue.s IL_0057 IL_002c: ldloc.0 IL_002d: ldstr "four" IL_0032: call bool [mscorlib]System.String::op_Equality(string, string) IL_0037: brtrue.s IL_005f IL_0039: br.s IL_0067 IL_003b: ldstr "<null>" IL_0040: call void [mscorlib]System.Console::WriteLine(string) IL_0045: br.s IL_0071 IL_0047: ldc.i4.1 IL_0048: call void [mscorlib]System.Console::WriteLine(int32) IL_004d: br.s IL_0071 IL_004f: ldc.i4.2 IL_0050: call void [mscorlib]System.Console::WriteLine(int32) IL_0055: br.s IL_0071 IL_0057: ldc.i4.3 IL_0058: call void [mscorlib]System.Console::WriteLine(int32) IL_005d: br.s IL_0071 IL_005f: ldc.i4.4 IL_0060: call void [mscorlib]System.Console::WriteLine(int32) IL_0065: br.s IL_0071 IL_0067: ldstr "<Unknown>" IL_006c: call void [mscorlib]System.Console::WriteLine(string) IL_0071: ldstr "After switch." IL_0076: call void [mscorlib]System.Console::WriteLine(string) IL_007b: ret } // end of method Program::TestSwitchString
呵呵,第一感觉就是,没见到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语句
static void TestSwitchString2(string s) { switch (s) { case null: Console.WriteLine("<null>"); break; case "one": Console.WriteLine(1); break; case "two": Console.WriteLine(2); break; case "three": Console.WriteLine(3); break; case "four": Console.WriteLine(4); break; case "five": Console.WriteLine(5); break; default: Console.WriteLine("<Unknown>"); break; } Console.WriteLine("After switch."); }
哈哈,老刘不厚道啊,不就多了一个 case "five"子句么。
是的,就多这一个。下面我们来看一下代码 3对应的 IL代码。
代码 4 -代码 3对应的 IL代码
.method private hidebysig static void TestSwitchString2(string s) cil managed { // Code size 205 (0xcd) .maxstack 4 .locals init (string V_0, int32 V_1) IL_0000: ldarg.0 IL_0001: dup IL_0002: stloc.0 IL_0003: brfalse.s IL_0084 IL_0005: volatile. IL_0007: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32> '<PrivateImplementationDetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA}'::'$$method0x6000007-1' IL_000c: brtrue.s IL_0057 IL_000e: ldc.i4.5 IL_000f: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::.ctor(int32) IL_0014: dup IL_0015: ldstr "one" IL_001a: ldc.i4.0 IL_001b: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::Add(!0, !1) IL_0020: dup IL_0021: ldstr "two" IL_0026: ldc.i4.1 IL_0027: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::Add(!0, !1) IL_002c: dup IL_002d: ldstr "three" IL_0032: ldc.i4.2 IL_0033: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::Add(!0, !1) IL_0038: dup IL_0039: ldstr "four" IL_003e: ldc.i4.3 IL_003f: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::Add(!0, !1) IL_0044: dup IL_0045: ldstr "five" IL_004a: ldc.i4.4 IL_004b: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::Add(!0, !1) IL_0050: volatile. IL_0052: stsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32> '<PrivateImplementationDetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA}'::'$$method0x6000007-1' IL_0057: volatile. IL_0059: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32> '<PrivateImplementationDetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA}'::'$$method0x6000007-1' IL_005e: ldloc.0 IL_005f: ldloca.s V_1 IL_0061: call instance bool class [mscorlib]System.Collections.Generic.Dictionary`2<string,int32>::TryGetValue(!0, !1&) IL_0066: brfalse.s IL_00b8 IL_0068: ldloc.1 IL_0069: switch ( IL_0090, IL_0098, IL_00a0, IL_00a8, IL_00b0) IL_0082: br.s IL_00b8 IL_0084: ldstr "<null>" IL_0089: call void [mscorlib]System.Console::WriteLine(string) IL_008e: br.s IL_00c2 IL_0090: ldc.i4.1 IL_0091: call void [mscorlib]System.Console::WriteLine(int32) IL_0096: br.s IL_00c2 IL_0098: ldc.i4.2 IL_0099: call void [mscorlib]System.Console::WriteLine(int32) IL_009e: br.s IL_00c2 IL_00a0: ldc.i4.3 IL_00a1: call void [mscorlib]System.Console::WriteLine(int32) IL_00a6: br.s IL_00c2 IL_00a8: ldc.i4.4 IL_00a9: call void [mscorlib]System.Console::WriteLine(int32) IL_00ae: br.s IL_00c2 IL_00b0: ldc.i4.5 IL_00b1: call void [mscorlib]System.Console::WriteLine(int32) IL_00b6: br.s IL_00c2 IL_00b8: ldstr "<Unknown>" IL_00bd: call void [mscorlib]System.Console::WriteLine(string) IL_00c2: ldstr "After switch." IL_00c7: call void [mscorlib]System.Console::WriteLine(string) IL_00cc: ret } // end of method Program::TestSwitchString2
耶?有奇怪的东西出现。你是不是第一眼也看到了 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进行判断。
本文来源: