透過IL看C# (2)——switch語句(下)

來源:互聯網
上載者:User
透過IL看C# (2)
switch語句(下)

原文地址:http://www.cnblogs.com/AndersLiu/archive/2008/11/06/csharp-via-il-switch-2.html

原創:Anders Liu

摘要:switch語句是C#中常用的跳躍陳述式,可以根據一個參數的不同取值執行不同的代碼。本文介紹了當向switch語句中傳入不同類型的參數時,編譯器為其產生的IL代碼。這一部分介紹的是,在switch語句中使用字串類型的情況。

之前我們介紹了在switch語句中使用整數類型和枚舉類型的情況。這一部分繼續介紹使用string類型的情況。string類型是switch語句接受的唯一一種參考型別參數。

下面來看一段C#代碼。

代碼1 - 使用string型別參數的switch語句

<br />static void TestSwitchString(string s)<br />{<br />switch(s)<br />{<br />case null: Console.WriteLine("<null>"); break;<br />case "one": Console.WriteLine(1); break;<br />case "two": Console.WriteLine(2); break;<br />case "three": Console.WriteLine(3); break;<br />case "four": Console.WriteLine(4); break;<br />default: Console.WriteLine("<unknown>"); break;<br />}</p><p>Console.WriteLine("After switch.");<br />}<br /></unknown></null>

代碼1展示的方法中只有一個switch語句,它接收一個字串類型的參數s,並根據6種不同的情況顯示不同的文字。它將被編譯器翻譯成什麼樣子的代碼呢?這個switch語句是否依然能利用IL中的switch指令呢?

答案馬上揭曉。且看由代碼1得到的IL,如代碼2所示。

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

<br />.method private hidebysig static void TestSwitchString(string s) cil managed<br />{<br /> // Code size 124 (0x7c)<br /> .maxstack 2<br /> .locals init (string V_0)<br /> IL_0000: ldarg.0<br /> IL_0001: dup<br /> IL_0002: stloc.0<br /> IL_0003: brfalse.s IL_003b<br /> IL_0005: ldloc.0<br /> IL_0006: ldstr "one"<br /> IL_000b: call bool [mscorlib]System.String::op_Equality(string,<br /> string)<br /> IL_0010: brtrue.s IL_0047<br /> IL_0012: ldloc.0<br /> IL_0013: ldstr "two"<br /> IL_0018: call bool [mscorlib]System.String::op_Equality(string,<br /> string)<br /> IL_001d: brtrue.s IL_004f<br /> IL_001f: ldloc.0<br /> IL_0020: ldstr "three"<br /> IL_0025: call bool [mscorlib]System.String::op_Equality(string,<br /> string)<br /> IL_002a: brtrue.s IL_0057<br /> IL_002c: ldloc.0<br /> IL_002d: ldstr "four"<br /> IL_0032: call bool [mscorlib]System.String::op_Equality(string,<br /> string)<br /> IL_0037: brtrue.s IL_005f<br /> IL_0039: br.s IL_0067<br /> IL_003b: ldstr "<null>"<br /> IL_0040: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0045: br.s IL_0071<br /> IL_0047: ldc.i4.1<br /> IL_0048: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_004d: br.s IL_0071<br /> IL_004f: ldc.i4.2<br /> IL_0050: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_0055: br.s IL_0071<br /> IL_0057: ldc.i4.3<br /> IL_0058: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_005d: br.s IL_0071<br /> IL_005f: ldc.i4.4<br /> IL_0060: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_0065: br.s IL_0071<br /> IL_0067: ldstr "<unknown>"<br /> IL_006c: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0071: ldstr "After switch."<br /> IL_0076: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_007b: ret<br />} // end of method Program::TestSwitchString<br /></unknown></null>

呵呵,第一感覺就是,沒見到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語句

<br />static void TestSwitchString2(string s)<br />{<br /> switch (s)<br /> {<br /> case null: Console.WriteLine("<null>"); break;<br /> case "one": Console.WriteLine(1); break;<br /> case "two": Console.WriteLine(2); break;<br /> case "three": Console.WriteLine(3); break;<br /> case "four": Console.WriteLine(4); break;<br /> case "five": Console.WriteLine(5); break;<br /> default: Console.WriteLine("<unknown>"); break;<br /> }</p><p> Console.WriteLine("After switch.");<br />}<br /></unknown></null>

哈哈,老劉不厚道啊,不就多了一個case "five"子句麼。

是的,就多這一個。下面我們來看一下代碼3對應的IL代碼。

代碼4 - 代碼3對應的IL代碼

<br />.method private hidebysig static void TestSwitchString2(string s) cil managed<br />{<br /> // Code size 205 (0xcd)<br /> .maxstack 4<br /> .locals init (string V_0,<br /> int32 V_1)<br /> IL_0000: ldarg.0<br /> IL_0001: dup<br /> IL_0002: stloc.0<br /> IL_0003: brfalse.s IL_0084<br /> IL_0005: volatile.<br /> IL_0007: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string> '<privateimplementationdetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA}'::'$$method0x6000007-1'<br /> IL_000c: brtrue.s IL_0057<br /> IL_000e: ldc.i4.5<br /> IL_000f: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string>::.ctor(int32)<br /> IL_0014: dup<br /> IL_0015: ldstr "one"<br /> IL_001a: ldc.i4.0<br /> IL_001b: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string>::Add(!0,<br /> !1)<br /> IL_0020: dup<br /> IL_0021: ldstr "two"<br /> IL_0026: ldc.i4.1<br /> IL_0027: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string>::Add(!0,<br /> !1)<br /> IL_002c: dup<br /> IL_002d: ldstr "three"<br /> IL_0032: ldc.i4.2<br /> IL_0033: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string>::Add(!0,<br /> !1)<br /> IL_0038: dup<br /> IL_0039: ldstr "four"<br /> IL_003e: ldc.i4.3<br /> IL_003f: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string>::Add(!0,<br /> !1)<br /> IL_0044: dup<br /> IL_0045: ldstr "five"<br /> IL_004a: ldc.i4.4<br /> IL_004b: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string>::Add(!0,<br /> !1)<br /> IL_0050: volatile.<br /> IL_0052: stsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string> '<privateimplementationdetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA}'::'$$method0x6000007-1'<br /> IL_0057: volatile.<br /> IL_0059: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string> '<privateimplementationdetails>{16CB032A-D97A-40BA-84F1-233334FEF4FA}'::'$$method0x6000007-1'<br /> IL_005e: ldloc.0<br /> IL_005f: ldloca.s V_1<br /> IL_0061: call instance bool class [mscorlib]System.Collections.Generic.Dictionary`2<string>::TryGetValue(!0,<br /> !1&)<br /> IL_0066: brfalse.s IL_00b8<br /> IL_0068: ldloc.1<br /> IL_0069: switch (<br /> IL_0090,<br /> IL_0098,<br /> IL_00a0,<br /> IL_00a8,<br /> IL_00b0)<br /> IL_0082: br.s IL_00b8<br /> IL_0084: ldstr "<null>"<br /> IL_0089: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_008e: br.s IL_00c2<br /> IL_0090: ldc.i4.1<br /> IL_0091: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_0096: br.s IL_00c2<br /> IL_0098: ldc.i4.2<br /> IL_0099: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_009e: br.s IL_00c2<br /> IL_00a0: ldc.i4.3<br /> IL_00a1: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_00a6: br.s IL_00c2<br /> IL_00a8: ldc.i4.4<br /> IL_00a9: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_00ae: br.s IL_00c2<br /> IL_00b0: ldc.i4.5<br /> IL_00b1: call void [mscorlib]System.Console::WriteLine(int32)<br /> IL_00b6: br.s IL_00c2<br /> IL_00b8: ldstr "<unknown>"<br /> IL_00bd: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_00c2: ldstr "After switch."<br /> IL_00c7: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_00cc: ret<br />} // end of method Program::TestSwitchString2<br /></unknown></null></string></privateimplementationdetails></string></privateimplementationdetails></string></string></string></string></string></string></string></privateimplementationdetails></string>

耶?有奇怪的東西出現。你是不是第一眼也看到了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進行判斷。

返回目錄:透過IL看C#

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.