1. 摘要
在這篇文章中,我會通過IL去分析一個簡單的語句。
如果覺得實在簡單,可以略過。
2. 引子
事情是這樣的,同事寫了一段類似這樣的代碼:
class Program{ static void Main(string[] args) { object o = new object(); int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : 3.5; }}
當然不是在控制台程式中,我在這裡只是寫出個類比。
然後系統報出了一個這樣的錯誤。
3. 錯誤分析
同事很詫異地問我,這是為什麼啊?
他給出的理由是object是一切類的父類,那麼我把3.5或者null賦給他都沒有問題啊,那這個問題是怎麼回事呢?
我意識到自己的語言表達能力遠不如代碼有說服力,於是,寫段代碼,然後請出IL。
4. 請出IL
讓我們先寫段正確的代碼,保證他的編譯通過。
class Program{ static void Main(string[] args) { object o = new object(); int i = 1; int j = 2; o = i+j > 3 ? 3 : 3.5; }}
然後去查看IL代碼:
.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint .maxstack 2 .locals init ( [0] object o, [1] int32 i, [2] int32 j) L_0000: nop L_0001: newobj instance void [mscorlib]System.Object::.ctor() L_0006: stloc.0 L_0007: ldc.i4.1 L_0008: stloc.1 L_0009: ldc.i4.2 L_000a: stloc.2 L_000b: ldloc.1 L_000c: ldloc.2 L_000d: add L_000e: ldc.i4.3 L_000f: bgt.s L_001c L_0011: ldc.r8 3.5 L_001a: br.s L_0025 L_001c: ldc.r8 3 L_0025: box float64 L_002a: stloc.0 L_002b: ret}
在這裡,我們只關注從L_00d開始的代碼,首先我們將兩個數i和j相加,然後去與3比較大小,如果大於3,那麼便跳轉到L_001c,將3作為Float類型壓棧,否則順序向下執行,將3.5作為float類型壓棧。最後將棧頂元素裝箱。
看過了這個解釋,我們再回去看原有的那段代碼,原因再清楚不過了,?:這個三元運算子在編譯成IL代碼時,把:兩端的值壓棧,然後把這兩個值儲存在一個臨時變數裡,而這個變數要取兩者之前類型轉換後級數最高的類型。舉個例子:int 和 float 就需要轉換成float,float 和 double 就需要轉換成double 等等。而在同事的程式中, double 類型和 null 類型無法相互轉換,所以就報了這樣的一個錯誤。
5. 改造代碼
繼續仔細思考,究竟什麼樣的兩個類型可以寫在:的兩端。上面的錯誤再清楚不過。兩個可以隱式轉換的類型可以。
那下面繼續解決這個問題。上面的代碼我們要怎麼寫:
static void Main(string[] args){ object o = new object(); int i; Int32.TryParse(Console.ReadLine(), out i); if (i > 3) { o = null; } else { o = 3.5; }}
怎麼看都沒有上面的代碼漂亮。這個可以說除了能運行外真的沒什麼優點了。
還記得那個泛型類吧:Nullable<T>。
那就讓我們用這個泛型類來改造吧:
static void Main(string[] args){ object o = new object(); Nullable<double> n = 3.5; int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : n;}
如果覺得Nullable<T>還不夠美觀。
static void Main(string[] args){ object o = new object(); double? n = 3.5; int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : n;}
這樣改造是不是優秀了一些呢?
這個時候如果有人提出,那麼我為什麼不o=i>3?null:(object)3.5呢?那我們想一下如果有一天我們不再用object o;
我們是不是可以把代碼寫成這樣:
static void Main(string[] args){ //object o = new object(); double? o; double? n = 3.5; int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : n;}
ShadowK 給出了這樣的做法,是個好辦法:
o = i > 3 ? (double?)null : n ;
這個時候再看IL,是不是已經沒有了可惡的box呢?
.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint .maxstack 2 .locals init ( [0] valuetype [mscorlib]System.Nullable`1<float64> o, [1] valuetype [mscorlib]System.Nullable`1<float64> n, [2] int32 i, [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0000) L_0000: nop L_0001: ldloca.s n L_0003: ldc.r8 3.5 L_000c: call instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0) L_0011: nop L_0012: call string [mscorlib]System.Console::ReadLine() L_0017: ldloca.s i L_0019: call bool [mscorlib]System.Int32::TryParse(string, int32&) L_001e: pop L_001f: ldloc.2 L_0020: ldc.i4.3 L_0021: bgt.s L_0026 L_0023: ldloc.1 L_0024: br.s L_002f L_0026: ldloca.s CS$0$0000 L_0028: initobj [mscorlib]System.Nullable`1<float64> L_002e: ldloc.3 L_002f: stloc.0 L_0030: ret }
6. 總結
寫上面的文章我並不是單純地想闡明這個具體的文法情況。而是希望大家掌握一種思路。
文法怎麼回事?為什麼不是像我想的那樣?
請出IL。