C#6 null 条件运算符

1. 老版本的代码

 namespace csharp6
 {
  internal class Person
  {
   public string Name { get; set; }
  }

  internal class Program
  {
   private static void Main()
   {
   Person person = null;
   string name = null;
   if (person != null)
   {
    name = person.Name;
   }
  }
 }
 }

在我们使用一个对象的属性的时候,有时候第一步需要做的事情是先判断这个对象本身是不是bull,不然的话你可能会得到一个System.NullReferenceException 的异常。虽然有时候我们可以使用三元运算符string name = person != null ? person.Name : null;来简化代码,但是这种书写方式还是不够简单......由于null值检测时编程中非常常用的一种编码行为,so,C#6为我们带来了一种更为简化的方式。

2. null条件运算符

 namespace csharp6
 {
  internal class Person
  {
   public string Name { get; set; }
  }

  internal class Program
  {
   private static void Main()
   {
    Person person = null;
   string name = person?.Name;
  }
  }
 }

从上面我们可以看出,使用?. 这种方式可以代替if判断和简化三元运算符的使用,简洁到不能再简洁了吧。按照惯例,上两份IL代码对比对比。

老版本的IL代码:

.method private hidebysig static void Main() cil managed
 {
 .entrypoint
 // Code size  23 (0x17)
 .maxstack 2
 .locals init ([0] class csharp6.Person person,
    [1] string name,
    [2] bool V_2)
 IL_0000: nop
 IL_0001: ldnull
 IL_0002: stloc.0
 IL_0003: ldnull
 IL_0004: stloc.1
 IL_0005: ldloc.0
 IL_0006: ldnull
 IL_0007: cgt.un
 IL_0009: stloc.2
 IL_000a: ldloc.2
 IL_000b: brfalse.s IL_0016
 IL_000d: nop
 IL_000e: ldloc.0
 IL_000f: callvirt instance string csharp6.Person::get_Name()
 IL_0014: stloc.1
 IL_0015: nop
 IL_0016: ret
 } // end of method Program::Main

if版的IL

新语法的IL:

.method private hidebysig static void Main() cil managed
 {
 .entrypoint
 // Code size  17 (0x11)
 .maxstack 1
 .locals init ([0] class csharp6.Person person,
    [1] string name)
 IL_0000: nop
 IL_0001: ldnull
 IL_0002: stloc.0
 IL_0003: ldloc.0
 IL_0004: brtrue.s IL_0009
 IL_0006: ldnull
 IL_0007: br.s  IL_000f
 IL_0009: ldloc.0
 IL_000a: call  instance string csharp6.Person::get_Name()
 IL_000f: stloc.1
 IL_0010: ret
 } // end of method Program::Main

null条件运算符版的IL

咦,貌似有很大不一样,我们再来一份三元运算符版的IL看看:

 .method private hidebysig static void Main() cil managed
 {
 .entrypoint
 // Code size  17 (0x11)
 .maxstack 1
 .locals init ([0] class csharp6.Person person,
    [1] string name)
 IL_0000: nop
 IL_0001: ldnull
 IL_0002: stloc.0
 IL_0003: ldloc.0
 IL_0004: brtrue.s IL_0009
 IL_0006: ldnull
 IL_0007: br.s  IL_000f
 IL_0009: ldloc.0
 IL_000a: callvirt instance string csharp6.Person::get_Name()
 IL_000f: stloc.1
 IL_0010: ret
 } // end of method Program::Main

三元运算符版的IL

新语法"?."和三元运算符"?:"的结果是唯一的差别是IL_000a这一行。"?."的方式被编译为call,而"?:"的方式被编译为callvirt,不知为何"?:"中的persion.Name为何会被编译成支持多态方式调用的callvirt,在这种情况下貌似call效率会更高一些,但是终究"?."和"?:"编译的代码没有本质差异。

但是和if判断的相比简化了一些,我们分析下IL,看看有哪些差异(这里就忽略call和callvirt的区别了):

if版的IL分析:

.method private hidebysig static void Main() cil managed
 {
 .entrypoint
 .maxstack 2
 .locals init ([0] class csharp6.Person person, //初始化局部变量person,把person放在索引为0的位置
   [1] string name,      //初始化局部变量name,把name放在索引为1的位置
   [2] bool V_2)       //初始化局部变量V_2,把V_2放在索引为2的位置
 IL_0000: nop         //空
 IL_0001: ldnull        //加载null
 IL_0002: stloc.0        //把null放入索引为0的变量,也就是person对象。
 IL_0003: ldnull        //加载null
 IL_0004: stloc.1        //把null放入索引为1的变量,也就是name对象。
 IL_0005: ldloc.0        //加载索引为0的位置的变量,也就是person对象
 IL_0006: ldnull        //加载null
 IL_0007: cgt.un        //比较前两步加载的值。如果第一个值大于第二个值,则将整数值1推送到计算堆栈上;反之,将0推送到计算堆栈上。
 IL_0009: stloc.2        //把比较结果放入索引为2的变量中,也就是V_2对象
 IL_000a: ldloc.2        //加载索引为2的对象,也就是V_2对象
 IL_000b: brfalse.s IL_0016     //如果上一步加载的对象为false、空引用或零,则跳转到IL_0016位置,也就是结束当前方法。
 IL_000d: nop         //空
 IL_000e: ldloc.0        //加载索引为0的位置的变量,也就是person对象
 IL_000f: callvirt instance string csharp6.Person::get_Name() //调用person对象的get_Name方法。
 IL_0014: stloc.1        //把上一步的结果存入索引为1的变量中,也就是name对象。
 IL_0015: nop         //空
 IL_0016: ret         //返回
 } 

null条件运算符版的IL分析:

 .method private hidebysig static void Main() cil managed
 {
  .entrypoint
  .maxstack 1
  .locals init ([0] class csharp6.Person person, //初始化局部变量person,把person放在索引为0的位置
       [1] string name)           //初始化局部变量name,把name放在索引为1的位置
  IL_0000: nop                 //空
  IL_0001: ldnull                //加载null
  IL_0002: stloc.0               //把null放入索引为0的变量,也就是person对象
  IL_0003: ldloc.0               //加载索引为0的位置的变量,也就是person对象
  IL_0004: brtrue.s  IL_0009          //如果上一步加载的对象为true、非空引用或非零,则跳转到IL_0009位置
  IL_0006: ldnull                //加载null
  IL_0007: br.s    IL_000f          //无条件的跳转到IL_000f处
  IL_0009: ldloc.0               //加载索引为0的位置的变量,也就是person对象
  IL_000a: call    instance string csharp6.Person::get_Name() ////调用person对象的get_Name方法。
  IL_000f: stloc.1               //把上一步的结果存入索引为1的变量中,也就是name对象。
  IL_0010: ret                 //返回
 }

通过分析我们发现,null运算符编译后的IL代码更简短,使用了2个分支跳转,简化了判断逻辑,而if版的IL还多出来一个bool类型的V_2临时变量。

so,结论就是"?."的和三元运算符"?:"的编译结果是一样的,而且简化了if的判断。所以不管是从性能还是可读性方面考虑,"?."都是推荐的写法。

3. Example 3.1 ?[

null条件运算符不但可以使用?.的语法访问对象的属性和方法,还可以用?[ 的语法访问检测数组或包含索引器的对象是否是null。比如:

 Person[] persons = null;
 //?.
 int? length = persons?.Length;
 //?[
 Person first = persons?[0];

3.2 ?.结合??

上面的persions?.Lenght返回的结果是Nullable类型的,有时候我们可能需要的是一个int类型的,那么我们可以结合空连接运算符"??"一起使用,比如:

Person[] persons = null;
 //?.和??结合使用
 int length = persons?.Length ?? 0;

3.3 以线程安全的方式调用事件

 PropertyChangedEventHandler propertyChanged = PropertyChanged;
 if (propertyChanged != null)
 {
 propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
 }

上面的代码一直是我们调用事件的处理方式,把事件的引用放到一个临时变量中是为了防止在调用这个委托的时候,事件被取消注册,产生null的情况。

我们从C#6以后终于可以用更简单的方式去触发事件调用了(这个埂自从C#1时代一直延续至今...):

PropertyChanged?.Invoke(propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));

4. 总结

null条件运算符是一种语法简化,同时也会做一种编译优化,优化方式和三元运算符的优化效果是一致的。语法更简化了,性能也更好了,我们有什么理由不用新语法呢。

(0)

相关推荐

  • C#6 null 条件运算符

    1. 老版本的代码 namespace csharp6 { internal class Person { public string Name { get; set; } } internal class Program { private static void Main() { Person person = null; string name = null; if (person != null) { name = person.Name; } } } } 在我们使用一个对象的属性的时候

  • 三十分钟快速掌握C# 6.0知识点

    一.只读自动属性(Read-only auto-properties) C# 6之前我们构建只读自动属性: public string FirstName { get; private set; } public string LastName { get; private set; } 原理解析:就是编译器在生成set访问器时,它的修饰符是private,由上可知所谓的只读只是针对类外部,在类内部还是可以随意修改属性值的. C# 6中提供了真正的只读自动属性,写法如下: public stri

  • C# 6.0 的知识梳理

    序 目前最新的版本是 C# 7.0,VS 的最新版本为 Visual Studio 2017 RC,两者都尚未进入正式阶段.C# 6.0 虽说出了一段时间,但是似乎有许多园友对这一块知识并不了解,如拼接字符串的 $ 符号,在此,小人献上拙作一篇<C# 6.0 的知识梳理>,祝大家在新的一年里:年年有今日,岁岁有今朝,月月涨工资,周周中彩票,天天好心情,日日好运道,白天遇财神,夜晚数钞票. 好了,废话不多说,我们先来回顾一下 C# 的版本史.后续我会对带 0 的版本号进行的简写:C# 6.0 -

  • C#6.0中你可能不知道的新特性总结

    为什么写? 今天去上班的公交上,有朋友在张队(张善友)的微信群里,发了一个介绍C# 6.0新特性的视频,视频7分钟,加上本人英语实在太low,整体看下来是一脸懵逼的. 下班回到家里,打开这个视频,把视频中介绍的新特性用文档的形式记录下来,加深自己的印象,此处把我整理的文档分享出来,希望对大家能有所帮助! C#6.0已经发布快三年了,可能我们没有有太去关心新版本所加入的特性,有人说,发布新版本,无非就是添加一些"语法糖"罢了,不管是糖不是糖,既然加入了新功能,那么自然有新功能的好处,我们

  • 遵守这些原则让你开发效率提高一倍(收藏)

    一.概述 在园子里面有很多关于各种技术细节的研究文章,都是比较牛逼的框架研究:但是一直没有看到关于怎么样提高开发效率的文章,大多提高开发效率的文章都是关于自动化等方面的辅助工具类型的,而不是开发中的一些小技巧:今天从编码规范.编码技巧.开发思想.设计模式等各方面的经验来分享如何提高开发效率. 二.实际场景 在这个前后端分离盛行的开发年代,分工比较明确,开发者分前端开发者和后端开发者,然而感到欣慰的是.net 开发者大多是担任着全栈开发的职责,有经验的开发者都是从前端走过来的,说白了前端业务代码对

  • C# 10个常用特性汇总

    1) async / await 使用 async / await 模式,可以在执行代码块操作的时候不会阻塞 UI 或者当前的线程.即使该操作被某些执行动作延迟了(比如一个 web 请求),async / await 模式也会继续执行后续的代码. 微软文档:https://msdn.microsoft.com/zh-cn/library/hh191443.aspx 2) 对象 / 数组 / 集合的初始值设定项(initializers) 通过使用对象.数组.集合的初始值设定项,可以很容易地创建类

  • [Asp.Net Core]提高开发效率的方法

    一.概述 在园子里面有很多关于各种技术细节的研究文章,都是比较牛逼的框架研究:但是一直没有看到关于怎么样提高开发效率的文章,大多提高开发效率的文章都是关于自动化等方面的辅助工具类型的,而不是开发中的一些小技巧:今天从编码规范.编码技巧.开发思想.设计模式等各方面的经验来分享如何提高开发效率. 二.实际场景 在这个前后端分离盛行的开发年代,分工比较明确,开发者分前端开发者和后端开发者,然而感到欣慰的是.net 开发者大多是担任着全栈开发的职责,有经验的开发者都是从前端走过来的,说白了前端业务代码对

  • C#6.0新语法示例详解

    前言 一直用C#开发程序,.NET的功能越来越多,变化也挺大的,从最初的封闭,到现在的开源,功能不断的增加,一直在进步.下面就来给大家详细介绍下C#6.0新语法的相关内容,一起来看看吧 众所周知,c# 6.0 是在visual studio 2015中引入的.在其他的几个版本中同样引入一些特性,比如在c# 3.0中引入了linq,在c# 4.0中引入了动态类型dynamic,在c#5.0中引入async和await等等. 在c# 6.0更多关注了语法的改进,而不是增加新的功能.这些新的语法将有助

  • C#中单问号(?)和双问号(??)的用法整理

    一.可空类型修饰符(?) C#2.0里面实现了Nullable数据类型 //A.比如下面一句,直接定义int为null是错误的,错误提示为无法将null转化成int,因为后者是不可以为null的值类型. private int getNum = null; //B.如果修改为下面的写法就可以初始指为null,在特定情况下?等同于基础类型为Nullable. private int? getNum = null; private Nullable<int> getNumNull = null;

  • Java中String判断值为null或空及地址是否相等的问题

    String的null或空值的判断处理 笔者在开发过程中,常常碰到过下面这些错误的用法: 1,错误用法一: if (name == "") { //do something } 2,错误用法二: if (name.equals("")) { //do something } 3,错误用法三: if (!name.equals("")) { //do something } 我们来解说一下: 上述错误用法1是初学者最容易犯,也最不容易被发现的错误,

随机推荐