8

Does using the null-conditional operator duplicate null checks? For example

var x = instance?.Property1;
var y = instance?.Property2;

Does that get compiled into this:

if (instance != null)
{
  var x = instance.Property1;
  var y = instance.Property2;
}

Or this?

if (instance != null)
{
  var x = instance.Property1;
}

if (instance != null)
{
  var y = instance.Property2;
}

If the former, does it make a difference if there is other code in between both lines? In other words, how smart is the compiler/optimizer?

user2864740
  • 60,010
  • 15
  • 145
  • 220
Nelson Rothermel
  • 9,436
  • 8
  • 62
  • 81

3 Answers3

6

The compiler appears to be quite ignorant of this.

The code:

var x = instance?.Property1;
var y = instance?.Property2;

...compiles as non-optimized to:

IL_0000:  nop         
IL_0001:  newobj      UserQuery+Class..ctor
IL_0006:  stloc.0     // instance
IL_0007:  ldloc.0     // instance
IL_0008:  brtrue.s    IL_000D
IL_000A:  ldnull      
IL_000B:  br.s        IL_0013
IL_000D:  ldloc.0     // instance
IL_000E:  ldfld       UserQuery+Class.Property1
IL_0013:  stloc.1     // x
IL_0014:  ldloc.0     // instance
IL_0015:  brtrue.s    IL_001A
IL_0017:  ldnull      
IL_0018:  br.s        IL_0020
IL_001A:  ldloc.0     // instance
IL_001B:  ldfld       UserQuery+Class.Property2
IL_0020:  stloc.2     // y
IL_0021:  ret         

Class..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  nop         
IL_0007:  ret         

...and to optimized as:

IL_0000:  newobj      UserQuery+Class..ctor
IL_0005:  dup         
IL_0006:  dup         
IL_0007:  brtrue.s    IL_000C
IL_0009:  pop         
IL_000A:  br.s        IL_0012
IL_000C:  ldfld       UserQuery+Class.Property1
IL_0011:  pop         
IL_0012:  dup         
IL_0013:  brtrue.s    IL_0017
IL_0015:  pop         
IL_0016:  ret         
IL_0017:  ldfld       UserQuery+Class.Property2
IL_001C:  pop         
IL_001D:  ret         

Class..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  ret         

Both clearly with two branch checks.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Hi, this is great! :) off-topic, but I just wonder how do you get the IL in that format? – Ian Jan 25 '16 at 03:23
  • 3
    @Ian Almost definitely from LINQPad :) – Rob Jan 25 '16 at 03:27
  • @Rob hi, thanks for the direction! :D I read the accepted answer on this: http://stackoverflow.com/questions/3326571/how-can-i-view-msil-cil-generated-by-c-sharp-compiler-why-is-it-called-assemb and trying to figure out what does it mean by `.text` in the PE file. But if there is an alternative way of looking at the assembly code (as the other answer in the link shows) I would just go and check that. :) – Ian Jan 25 '16 at 03:30
  • This is neat, but could you explain the important bits so those who don't understand this exactly can still gain knowledge from it to prove the point of the question? – mariocatch Jan 25 '16 at 03:32
  • 1
    @Ian I'm not extremely well versed on the structure of PE files, but I would assume `.text` is simply a section of the file (separate from embedded files, for example). I would just use ILSpy as recommended (or with small snippets, just compile it with LINQPad) – Rob Jan 25 '16 at 03:34
  • @Rob I see... Thanks for the direction. I will go and check to play around with it. :) – Ian Jan 25 '16 at 03:35
  • 2
    @Ian - Yes, this is from LINQPad. Don't leave home without it. I do most of my coding in there now. – Enigmativity Jan 25 '16 at 03:36
  • @Enigmativity haha, like it.. :D – Ian Jan 25 '16 at 03:36
  • @Ian - The key part to look at is the `brtrue.s`. That's doing the branch on the `null`. Since there are two when the two statements are back-to-back then the compiler isn't very smart. – Enigmativity Jan 25 '16 at 03:37
  • Kind of off topic, but couldn't you save one IL line (and file size) by merging the two pops in each branch? Basically, remove `IL_0009: pop` and change `IL_000A: br.s IL_0012` to `IL_000A: br.s IL_0011` – Nelson Rothermel Jan 25 '16 at 03:45
  • @Enigmativity, how do you get the ptimized IL from LIQPad? – Paulo Morgado Jan 25 '16 at 08:59
  • @PauloMorgado - Under preferences you can select between optimized and non-optimized. I generally leave it unoptimized as this enabled debugging. – Enigmativity Jan 25 '16 at 10:42
  • Thanks, @Enigmativity. Had never noticed that. – Paulo Morgado Jan 25 '16 at 11:37
5

It does two separate If..Else checks for the assignment using null conditional operator in your case. Below is a disassembled sample code

Source Code:

public class nulltest
{
    public void test()
    {
        var instance = new testclass();
        var x = instance?.prop1;
        var y = instance?.prop2;
    }
}

public class testclass
{
    public int prop1;
    public int prop2;
}

Disassembled code (ILSpy):

public class nulltest
{
    public void test()
    {
        testclass testclass = new testclass();
        if (testclass == null)
        {
            int? arg_20_0 = null;
        }
        else
        {
            new int?(testclass.prop1);
        }
        if (testclass == null)
        {
            int? arg_3A_0 = null;
        }
        else
        {
            new int?(testclass.prop2);
        }
    }
}

I used int as the type of the property but the above should hold true for any other case.

Martin
  • 644
  • 5
  • 11
5

The compiler is rigorous, think about this code.

class Tricky
{
    public int Property1
    {
        get
        {
            Program.instance = null;
            return 1;
        }
    }

    public int Property2
    {
        get
        {
            return 2;
        }
    }
}

class Program
{
    public static Tricky instance = new Tricky();

    public static void Main(string[] arg)
    {
        var x = instance?.Property1;
        var y = instance?.Property2;
        //what do you think the values of x,y
    }
}

The expected result is : x == 1, y is null. But if the compiler optimizes the code using one if statement, it throws a NullReferenceException. Which means, using one if statement, is not a smart optimization, it's not an optimization, because it's WRONG.

Cheng Chen
  • 42,509
  • 16
  • 113
  • 174
  • @NelsonRothermel True. However, a compiler shouldn't work under these kind of assumptions, the above code is pure legal from the language view. – Cheng Chen Jan 25 '16 at 04:34
  • In cases where the left-hand side of the operator could be modified by the right-hand side, it must clearly be re-evaluated, but that wouldn't imply that in places where the compiler could tell that the left-hand side couldn't be modified by evaluating the right (e.g. both it and the right-hand side were field accesses) a compiler couldn't use something like `var temp=instance; if (temp != null) { x=temp.Field1; y=temp.Field2; } else { x=null; y=null; }` – supercat Jan 29 '16 at 23:02