SoFunction
Updated on 2025-03-06

C#6 null conditional operator

1. Old version of code

 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 = ;
   }
  }
 }
 }

When we use the properties of an object, sometimes the first step is to determine whether the object itself is bullish, otherwise you may get an exception. Although sometimes we can use the ternary operator string name = person != null ? : null; to simplify the code, this writing method is still not simple enough... Due to a very commonly used encoding behavior in programming during null value detection, so, C#6 brings us a more simplified way.

2. null condition operator

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

From the above we can see that using?. This method can replace if judgment and simplify the use of ternary operators, so concise that it cannot be more concise. As usual, compare the previous two IL codes.

Older version of IL code:

.method private hidebysig static void Main() cil managed
 {
 .entrypoint
 // Code size  23 (0x17)
 .maxstack 2
 .locals init ([0] class  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: 
 IL_0009: stloc.2
 IL_000a: ldloc.2
 IL_000b:  IL_0016
 IL_000d: nop
 IL_000e: ldloc.0
 IL_000f: callvirt instance string ::get_Name()
 IL_0014: stloc.1
 IL_0015: nop
 IL_0016: ret
 } // end of method Program::Main

if version IL

IL of the new syntax:

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

null conditional operator version IL

Hey, it seems that there is a big difference. Let’s take a look at the IL version of the ternary operator:

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

IL of the ternary operator version

The only difference between the new syntax "?." and the ternary operator "?:" is the line IL_000a. The "?." method is compiled into a call, while the "?:" method is compiled into a callvirt. For some reason, why is the "?:" compiled into a callvirt that supports polymorphic calling. In this case, it seems that the call will be more efficient, but in the end, there is no essential difference between the compiled code.

However, compared with the if judgment, we analyze IL to see what differences are (the difference between call and callvirt is ignored here):

If version IL analysis:

.method private hidebysig static void Main() cil managed
 {
 .entrypoint
 .maxstack 2
 .locals init ([0] class  person, //Initialize the local variable person and place the person at the index 0   [1] string name,      //Initialize the local variable name and place the name at index 1   [2] bool V_2)       //Initialize the local variable V_2 and place V_2 at the index 2 IL_0000: nop         //null IL_0001: ldnull        //Load null IL_0002: stloc.0        //Put null into a variable indexed to 0, that is, a person object. IL_0003: ldnull        //Load null IL_0004: stloc.1        //Put null into a variable indexed to 1, that is, the name object. IL_0005: ldloc.0        //Load the variable at the position with index 0, that is, the person object IL_0006: ldnull        //Load null IL_0007:         //Compare the values ​​loaded in the first two steps.  If the first value is greater than the second value, the integer value 1 is pushed to the calculation stack; otherwise, 0 is pushed to the calculation stack. IL_0009: stloc.2        //Put the comparison result into a variable with index 2, that is, the V_2 object IL_000a: ldloc.2        //Load an object with index 2, that is, V_2 object IL_000b:  IL_0016     //If the object loaded in the previous step is false, empty reference or zero, it will jump to the IL_0016 position, which means the current method ends. IL_000d: nop         //null IL_000e: ldloc.0        //Load the variable at the position with index 0, that is, the person object IL_000f: callvirt instance string ::get_Name() //Calling the get_Name method of the person object. IL_0014: stloc.1        //Save the result of the previous step into a variable indexed to 1, that is, the name object. IL_0015: nop         //null IL_0016: ret         //return } 

IL analysis of null conditional operator version:

 .method private hidebysig static void Main() cil managed
 {
  .entrypoint
  .maxstack 1
  .locals init ([0] class  person, //Initialize the local variable person and place the person at the index 0       [1] string name)           //Initialize the local variable name and place the name at index 1  IL_0000: nop                 //null  IL_0001: ldnull                //Load null  IL_0002: stloc.0               //Put null into a variable with index 0, that is, a person object  IL_0003: ldloc.0               //Load the variable at the position with index 0, that is, the person object  IL_0004:   IL_0009          //If the object loaded in the previous step is true, non-null reference or non-zero, then jump to the IL_0009 position  IL_0006: ldnull                //Load null  IL_0007:     IL_000f          //Unconditionally jump to IL_000f  IL_0009: ldloc.0               //Load the variable at the position with index 0, that is, the person object  IL_000a: call    instance string ::get_Name() ////Calling the get_Name method of the person object.  IL_000f: stloc.1               //Save the result of the previous step into a variable indexed to 1, that is, the name object.  IL_0010: ret                 //return }

Through analysis, we found that the IL code compiled by the null operator is shorter, using 2 branches to simplify the judgment logic, and the if version of IL also has an additional V_2 temporary variable of bool type.

so, the conclusion is that the compilation result of "?." and the ternary operator "?:" is the same, and it simplifies the judgment of if. So whether it is in terms of performance or readability, "?." is the recommended way of writing.

3. Example 3.1 ?[

The null condition operator can not only access the properties and methods of the object using the syntax of ?., but also use the syntax of ?[ to access whether the array or object containing the indexer is null. for example:

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

3.2 ?.Combined??

The above-response?.Lenght returns the result of Nullable type. Sometimes what we may need is an int type. Then we can use it in conjunction with the null connection operator "??", for example:

 Person[] persons = null;
//?. Use in combination with??
 int length = persons?.Length ?? 0;

3.3 Calling events in a thread-safe way

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

The above code has always been the way we call events. Putting the reference to the event into a temporary variable is to prevent the event from being unregistered and null is generated when this delegate is called.

Since C#6, we can finally trigger event calls in a simpler way (this ridge has been going on since the C#1 era...):

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

4. Summary

The null conditional operator is a syntax simplification, and it will also be compiled and optimized. The optimization method is consistent with the optimization effect of the ternary operator. The syntax is more simplified and the performance is better. What reason do we have to avoid using the new syntax?