透過IL看C# (1)——switch語句(上)

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

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

原創:Anders Liu

摘要: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語句,取值連續

<br />static void TestSwitchInt(int n)<br />{<br />switch(n)<br />{<br />case 1: Console.WriteLine("One"); break;<br />case 2: Console.WriteLine("Two"); break;<br />case 3: Console.WriteLine("Three"); break;<br />}<br />}<br />

代碼1中的switch語句接受的參數n是int類型的,並且我們觀察到,在各個case子句中的取值都是連續的。將這段代碼寫在一個完整的程式中,並進行編譯。之後使用ildasm開啟產生的程式集,可以看到對應的IL代碼如代碼2所示。

代碼2 – 代碼1產生的IL代碼

<br />.method private hidebysig static void TestSwitchInt(int32 n) cil managed<br />{<br /> // Code size 56 (0x38)<br /> .maxstack 2<br /> .locals init (int32 V_0)<br /> IL_0000: ldarg.0<br /> IL_0001: stloc.0<br /> IL_0002: ldloc.0<br /> IL_0003: ldc.i4.1<br /> IL_0004: sub<br /> IL_0005: switch (<br /> IL_0017,<br /> IL_0022,<br /> IL_002d)<br /> IL_0016: ret<br /> IL_0017: ldstr "One"<br /> IL_001c: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0021: ret<br /> IL_0022: ldstr "Two"<br /> IL_0027: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_002c: ret<br /> IL_002d: ldstr "Three"<br /> IL_0032: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0037: ret<br />} // end of method Program::TestSwitchInt<br />

我們可以看到,首先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語句,取值不連續

<br />static void TestSwitchInt2(int n)<br />{<br />switch(n)<br />{<br />case 1: Console.WriteLine("1"); break;<br />case 3: Console.WriteLine("3"); break;<br />case 5: Console.WriteLine("5"); break;<br />}<br />}<br />

代碼3編譯產生的程式集中,編譯器產生的IL代碼如下:

代碼4 – 代碼3產生的IL代碼

<br />.method private hidebysig static void TestSwitchInt2(int32 n) cil managed<br />{<br /> // Code size 64 (0x40)<br /> .maxstack 2<br /> .locals init (int32 V_0)<br /> IL_0000: ldarg.0<br /> IL_0001: stloc.0<br /> IL_0002: ldloc.0<br /> IL_0003: ldc.i4.1<br /> IL_0004: sub<br /> IL_0005: switch (<br /> IL_001f, // 0<br /> IL_003f, // 1<br /> IL_002a, // 2<br /> IL_003f, // 3<br /> IL_0035) // 4<br /> IL_001e: ret<br /> IL_001f: ldstr "1"<br /> IL_0024: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0029: ret<br /> IL_002a: ldstr "3"<br /> IL_002f: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0034: ret<br /> IL_0035: ldstr "5"<br /> IL_003a: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_003f: ret<br />} // end of method Program::TestSwitchInt2<br />

看到代碼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語句,取值非常不連續

<br />static void TestSwitchInt3(int n)<br />{<br />switch(n)<br />{<br />case 10: Console.WriteLine("10"); break;<br />case 30: Console.WriteLine("30"); break;<br />case 50: Console.WriteLine("50"); break;<br />}<br />}<br />

在代碼5中,switch語句的每個case子句中給出的取值之間都相差20,這意味著如果再採用前面所述“縫隙值”的做法,switch指令中將有多達41個跳轉地址,而其中有效只有3個。但現代的編譯器明顯不會犯這種低級錯誤。下面給出編譯器為代碼5 產生的IL:

代碼6 – 代碼5產生的IL代碼

<br />.method private hidebysig static void TestSwitchInt3(int32 n) cil managed<br />{<br /> // Code size 51 (0x33)<br /> .maxstack 2<br /> .locals init (int32 V_0)<br /> IL_0000: ldarg.0<br /> IL_0001: stloc.0<br /> IL_0002: ldloc.0<br /> IL_0003: ldc.i4.s 10<br /> IL_0005: beq.s IL_0012<br /> IL_0007: ldloc.0<br /> IL_0008: ldc.i4.s 30<br /> IL_000a: beq.s IL_001d<br /> IL_000c: ldloc.0<br /> IL_000d: ldc.i4.s 50<br /> IL_000f: beq.s IL_0028<br /> IL_0011: ret<br /> IL_0012: ldstr "10"<br /> IL_0017: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_001c: ret<br /> IL_001d: ldstr "30"<br /> IL_0022: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0027: ret<br /> IL_0028: ldstr "50"<br /> IL_002d: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0032: ret<br />} // end of method Program::TestSwitchInt3<br />

從代碼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語句中使用枚舉類型

<br />static void TestSwitchEnum(Num n)<br />{<br />switch(n)<br />{<br />case Num.One: Console.WriteLine("1"); break;<br />case Num.Two: Console.WriteLine("2"); break;<br />case Num.Three: Console.WriteLine("3"); break;<br />}<br />}<br />

其中的Num類型是一個枚舉,定義為public enum Num { One, Two, Three }

下面是編譯器為代碼7產生的IL代碼:

代碼8 - 代碼7產生的IL代碼

<br />.method private hidebysig static void TestSwitchEnum(valuetype AndersLiu.CSharpViaIL.Switch.Num n) cil managed<br />{<br /> // Code size 54 (0x36)<br /> .maxstack 1<br /> .locals init (valuetype AndersLiu.CSharpViaIL.Switch.Num V_0)<br /> IL_0000: ldarg.0<br /> IL_0001: stloc.0<br /> IL_0002: ldloc.0<br /> IL_0003: switch (<br /> IL_0015,<br /> IL_0020,<br /> IL_002b)<br /> IL_0014: ret<br /> IL_0015: ldstr "1"<br /> IL_001a: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_001f: ret<br /> IL_0020: ldstr "2"<br /> IL_0025: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_002a: ret<br /> IL_002b: ldstr "3"<br /> IL_0030: call void [mscorlib]System.Console::WriteLine(string)<br /> IL_0035: ret<br />} // end of method Program::TestSwitchEnum<br />

可以看到,代碼8和代碼2沒有什麼本質區別。這是因為枚舉值就是按照整數對待的。並且,如果枚舉定義的成員取值不連續,產生的程式碼也會和代碼4、代碼6類似。

小結

本文介紹了編譯器如何翻譯使用整數類型的switch語句。如果你很在乎微乎其微的效率提升的話,應記得:

  • 盡量在switch中使用連續的取值;
  • 如果取值不連續,則使用盡量少的case子句,並將出現頻率高的case放在前面(因為此時switch語句和if-else if-else語句是類似的)。

返回目錄:透過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.