[C#6] 3-null 条件运算符

简介: 0. 目录 C#6 新增特性目录 1. 老版本的代码 1 namespace csharp6 2 { 3 internal class Person 4 { 5 public string Name { get; set; } 6 ...

0. 目录

C#6 新增特性目录

1. 老版本的代码

 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为我们带来了一种更为简化的方式。

2. null条件运算符

 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的判断。所以不管是从性能还是可读性方面考虑,"?."都是推荐的写法。

3. Example

3.1 ?[

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

1 Person[] persons = null;
2 //?.
3 int? length = persons?.Length;
4 //?[
5 Person first = persons?[0];

3.2 ?.结合??

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

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

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

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)));

4. 总结

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

5. 参考

C#-Reference-Operators:Null-conditional Operators

作者: Blackheart
目录
相关文章
|
3天前
|
编译器 C#
c# - 运算符<<不能应用于long和long类型的操作数
在C#中,左移运算符的第二个操作数必须是 `int`类型,因此需要将 `long`类型的位移计数显式转换为 `int`类型。这种转换需要注意数据丢失和负值处理的问题。通过本文的详细说明和示例代码,相信可以帮助你在实际开发中正确使用左移运算符。
11 3
|
2天前
|
编译器 C#
c# - 运算符<<不能应用于long和long类型的操作数
在C#中,左移运算符的第二个操作数必须是 `int`类型,因此需要将 `long`类型的位移计数显式转换为 `int`类型。这种转换需要注意数据丢失和负值处理的问题。通过本文的详细说明和示例代码,相信可以帮助你在实际开发中正确使用左移运算符。
10 1
|
26天前
|
C# 开发者
【捞底干货】C#中equals和==运算符的区别
【捞底干货】C#中equals和==运算符的区别
15 1
|
5月前
|
C# 数据库
C#中的空合并运算符与空合并赋值运算符:简化空值处理
C#中的空合并运算符与空合并赋值运算符:简化空值处理
|
6月前
|
开发框架 .NET 程序员
C#三元运算符使用技巧
C#三元运算符使用技巧
50 0
|
6月前
|
C# 数据安全/隐私保护
C#运算符
C#运算符
33 0
|
6月前
|
C#
C# 运算符详解:包含算术、赋值、比较、逻辑运算符及 Math 类应用
运算符用于对变量和值执行操作。在C#中,有多种运算符可用,包括算术运算符、关系运算符、逻辑运算符等。
76 1
|
6月前
|
C# 图形学
【Unity 3D】C#控制语句break、continue及算数运算符和逻辑运算符的讲解(附测试代码)
【Unity 3D】C#控制语句break、continue及算数运算符和逻辑运算符的讲解(附测试代码)
58 1
|
6月前
|
存储 C#
C#入门开发(Hello World,运算符)
C#入门开发(Hello World,运算符)
52 0
|
6月前
|
存储 Java C#
【从Java转C#】第七章:运算符和类型强制转换
【从Java转C#】第七章:运算符和类型强制转换