1. 程式人生 > >switch語句(上)(轉載)

switch語句(上)(轉載)

goto語句 ret 開始 單獨 類型 -c C# 成了 clas

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語句(上)(轉載)