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語句,取值連續
代碼1中的switch語句接受的參數n是int類型的,並且我們觀察到,在各個case子句中的取值都是連續的。將這段代碼寫在一個完整的程序中,並進行編譯。之後使用ildasm打開生成的程序集,可以看到對應的IL代碼如代碼2所示。
代碼2 – 代碼1生成的IL代碼
我們可以看到,首先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語句,取值不連續
代碼3編譯生成的程序集中,編譯器生成的IL代碼如下:
代碼4 – 代碼3生成的IL代碼
看到代碼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語句,取值非常不連續
在代碼5中,switch語句的每個case子句中給出的取值之間都相差20,這意味著如果再采用前面所述“縫隙值”的做法,switch指令中將有多達41個跳轉地址,而其中有效的只有3個。但現代的編譯器明顯不會犯這種低級錯誤。下面給出編譯器為代碼5 生成的IL:
代碼6 – 代碼5生成的IL代碼
從代碼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語句中使用枚舉類型
其中的Num類型是一個枚舉,定義為public enum Num { One, Two, Three }
下面是編譯器為代碼7生成的IL代碼:
代碼8 - 代碼7生成的IL代碼
可以看到,代碼8和代碼2沒有什麽本質區別。這是因為枚舉值就是按照整數對待的。並且,如果枚舉定義的成員取值不連續,生成的代碼也會和代碼4、代碼6類似。
小結
本文介紹了編譯器如何翻譯使用整數類型的switch語句。如果你很在乎微乎其微的效率提升的話,應記得:
- 盡量在switch中使用連續的取值;
- 如果取值不連續,則使用盡量少的case子句,並將出現頻率高的case放在前面(因為此時switch語句和if-else if-else語句是類似的)。
switch語句(上)(轉載)