C#6 新增特性目錄
1 namespace csharp6 2 { 3 internal class Person 4 { 5 public string Name { get; set; } 6 } 7 8 internal class Program 9 { 10 private static void Main() 11 { 12 Person person = null; 13 //if判斷 14 string name = null; 15 if (person != null) 16 { 17 name = person.Name; 18 } 19 } 20 } 21 }
在我們使用一個對象的屬性的時候,有時候第一步需要做的事情是先判斷這個對象本身是不是bull,不然的話你可能會得到一個 System.NullReferenceException 的異常。雖然有時候我們可以使用三元運算符 string name = person != null ? person.Name : null; 來簡化代碼,但是這種書寫方式還是不夠簡單......由於null值檢測時編程中非常常用的一種編碼行為,so,C#6為我們帶來了一種更為簡化的方式。
1 namespace csharp6 2 { 3 internal class Person 4 { 5 public string Name { get; set; } 6 } 7 8 internal class Program 9 { 10 private static void Main() 11 { 12 Person person = null; 13 string name = person?.Name; 14 } 15 } 16 }
從上面我們可以看出,使用 ?. 這種方式可以代替if判斷和簡化三元運算符的使用,簡潔到不能再簡潔了吧。按照慣例,上兩份IL代碼對比對比。
老版本的IL代碼:
1 .method private hidebysig static void Main() cil managed 2 { 3 .entrypoint 4 // Code size 23 (0x17) 5 .maxstack 2 6 .locals init ([0] class csharp6.Person person, 7 [1] string name, 8 [2] bool V_2) 9 IL_0000: nop 10 IL_0001: ldnull 11 IL_0002: stloc.0 12 IL_0003: ldnull 13 IL_0004: stloc.1 14 IL_0005: ldloc.0 15 IL_0006: ldnull 16 IL_0007: cgt.un 17 IL_0009: stloc.2 18 IL_000a: ldloc.2 19 IL_000b: brfalse.s IL_0016 20 IL_000d: nop 21 IL_000e: ldloc.0 22 IL_000f: callvirt instance string csharp6.Person::get_Name() 23 IL_0014: stloc.1 24 IL_0015: nop 25 IL_0016: ret 26 } // end of method Program::Main if版的IL新語法的IL:
1 .method private hidebysig static void Main() cil managed 2 { 3 .entrypoint 4 // Code size 17 (0x11) 5 .maxstack 1 6 .locals init ([0] class csharp6.Person person, 7 [1] string name) 8 IL_0000: nop 9 IL_0001: ldnull 10 IL_0002: stloc.0 11 IL_0003: ldloc.0 12 IL_0004: brtrue.s IL_0009 13 IL_0006: ldnull 14 IL_0007: br.s IL_000f 15 IL_0009: ldloc.0 16 IL_000a: call instance string csharp6.Person::get_Name() 17 IL_000f: stloc.1 18 IL_0010: ret 19 } // end of method Program::Main null條件運算符版的IL咦,貌似有很大不一樣,我們再來一份三元運算符版的IL看看:
1 .method private hidebysig static void Main() cil managed 2 { 3 .entrypoint 4 // Code size 17 (0x11) 5 .maxstack 1 6 .locals init ([0] class csharp6.Person person, 7 [1] string name) 8 IL_0000: nop 9 IL_0001: ldnull 10 IL_0002: stloc.0 11 IL_0003: ldloc.0 12 IL_0004: brtrue.s IL_0009 13 IL_0006: ldnull 14 IL_0007: br.s IL_000f 15 IL_0009: ldloc.0 16 IL_000a: callvirt instance string csharp6.Person::get_Name() 17 IL_000f: stloc.1 18 IL_0010: ret 19 } // end of method Program::Main 三元運算符版的IL新語法"?."和三元運算符"?:"的結果是唯一的差別是IL_000a這一行。"?."的方式被編譯為call,而"?:"的方式被編譯為callvirt,不知為何"?:"中的persion.Name為何會被編譯成支持多態方式調用的callvirt,在這種情況下貌似call效率會更高一些,但是終究"?."和"?:"編譯的代碼沒有本質差異。
但是和if判斷的相比簡化了一些,我們分析下IL,看看有哪些差異(這裡就忽略call和callvirt的區別了):
if版的IL分析:
1 .method private hidebysig static void Main() cil managed 2 { 3 .entrypoint 4 .maxstack 2 5 .locals init ([0] class csharp6.Person person, //初始化局部變量person,把person放在索引為0的位置 6 [1] string name, //初始化局部變量name,把name放在索引為1的位置 7 [2] bool V_2) //初始化局部變量V_2,把V_2放在索引為2的位置 8 IL_0000: nop //空 9 IL_0001: ldnull //加載null 10 IL_0002: stloc.0 //把null放入索引為0的變量,也就是person對象。 11 IL_0003: ldnull //加載null 12 IL_0004: stloc.1 //把null放入索引為1的變量,也就是name對象。 13 IL_0005: ldloc.0 //加載索引為0的位置的變量,也就是person對象 14 IL_0006: ldnull //加載null 15 IL_0007: cgt.un //比較前兩步加載的值。如果第一個值大於第二個值,則將整數值1推送到計算堆棧上;反之,將0推送到計算堆棧上。 16 IL_0009: stloc.2 //把比較結果放入索引為2的變量中,也就是V_2對象 17 IL_000a: ldloc.2 //加載索引為2的對象,也就是V_2對象 18 IL_000b: brfalse.s IL_0016 //如果上一步加載的對象為false、空引用或零,則跳轉到IL_0016位置,也就是結束當前方法。 19 IL_000d: nop //空 20 IL_000e: ldloc.0 //加載索引為0的位置的變量,也就是person對象 21 IL_000f: callvirt instance string csharp6.Person::get_Name() //調用person對象的get_Name方法。 22 IL_0014: stloc.1 //把上一步的結果存入索引為1的變量中,也就是name對象。 23 IL_0015: nop //空 24 IL_0016: ret //返回 25 }
null條件運算符版的IL分析:
1 .method private hidebysig static void Main() cil managed 2 { 3 .entrypoint 4 .maxstack 1 5 .locals init ([0] class csharp6.Person person, //初始化局部變量person,把person放在索引為0的位置 6 [1] string name) //初始化局部變量name,把name放在索引為1的位置 7 IL_0000: nop //空 8 IL_0001: ldnull //加載null 9 IL_0002: stloc.0 //把null放入索引為0的變量,也就是person對象 10 IL_0003: ldloc.0 //加載索引為0的位置的變量,也就是person對象 11 IL_0004: brtrue.s IL_0009 //如果上一步加載的對象為true、非空引用或非零,則跳轉到IL_0009位置 12 IL_0006: ldnull //加載null 13 IL_0007: br.s IL_000f //無條件的跳轉到IL_000f處 14 IL_0009: ldloc.0 //加載索引為0的位置的變量,也就是person對象 15 IL_000a: call instance string csharp6.Person::get_Name() ////調用person對象的get_Name方法。 16 IL_000f: stloc.1 //把上一步的結果存入索引為1的變量中,也就是name對象。 17 IL_0010: ret //返回 18 }
通過分析我們發現,null運算符編譯後的IL代碼更簡短,使用了2個分支跳轉,簡化了判斷邏輯,而if版的IL還多出來一個bool類型的V_2臨時變量。
so,結論就是"?."的和三元運算符"?:"的編譯結果是一樣的,而且簡化了if的判斷。所以不管是從性能還是可讀性方面考慮,"?."都是推薦的寫法。
null條件運算符不但可以使用 ?. 的語法訪問對象的屬性和方法,還可以用 ?[ 的語法訪問檢測數組或包含索引器的對象是否是null。比如:
1 Person[] persons = null; 2 //?. 3 int? length = persons?.Length; 4 //?[ 5 Person first = persons?[0];
上面的persions?.Lenght返回的結果是Nullable<int>類型的,有時候我們可能需要的是一個int類型的,那麼我們可以結合空連接運算符"??"一起使用,比如:
1 Person[] persons = null; 2 //?.和??結合使用 3 int length = persons?.Length ?? 0;
1 PropertyChangedEventHandler propertyChanged = PropertyChanged; 2 if (propertyChanged != null) 3 { 4 propertyChanged(this, new PropertyChangedEventArgs(nameof(Name))); 5 }
上面的代碼一直是我們調用事件的處理方式,把事件的引用放到一個臨時變量中是為了防止在調用這個委托的時候,事件被取消注冊,產生null的情況。
我們從C#6以後終於可以用更簡單的方式去觸發事件調用了(這個埂自從C#1時代一直延續至今...):
1 PropertyChanged?.Invoke(propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
null條件運算符是一種語法簡化,同時也會做一種編譯優化,優化方式和三元運算符的優化效果是一致的。語法更簡化了,性能也更好了,我們有什麼理由不用新語法呢。
C#-Reference-Operators:Null-conditional Operators