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。