June 2, 2018

C# Null-Conditional and IL

So I was writing some code like the following:

if (service != null)
{
    service.Close();
}

Then I remembered, I can use Null-Conditional (yes, I know it's been out for a while with C# 6.0, hard to break force-of-habit):

service?.Close();

Clean and simple, but I wondered, is it really the same? So I fired up sharplab.io, and confirmed it's the same...or is it?

Let's go one step further. Fired up dotnetfiddle.net and checked the disassembled IL (my comments below on IL were added after skimming through Getting Started with IL documentation and some other sources).

Using null-conditional:

...
05. public static void Main()
06. {
07.     Service s = new Service();
08. 
09.     s?.Close();
10. }
...

IL:

...
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  1
    .locals init ([0] class Service s) // Create local variable '0' of type Service.
    .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
    .line 6,6 : 2,3 ''
    IL_0000:  nop
    .line 7,7 : 3,29 ''
    IL_0001:  newobj     instance void Service::.ctor() // Create new object, reference pushed to stack.
    IL_0006:  stloc.0    // Store to Local: Pop stack into local variable '0'.
    .line 9,9 : 9,20 ''
    IL_0007:  ldloc.0    // Load from Local: Push local variable '0' to stack.
    IL_0008:  brtrue.s   IL_000c  // If top of the stack is not zero, branch to IL_000c, pop stack.

    IL_000a:  br.s       IL_0013  // Branch to IL_0013

    IL_000c:  ldloc.0    // Load from Local: Push local variable '0' to stack.
    IL_000d:  call       instance void Service::Close()
    IL_0012:  nop
    .line 10,10 : 2,3 ''
    IL_0013:  ret        // Return.
  } // end of method Program::Main
...

Using if statement:

...
05. public static void Main()
06. {
07.     Service s = new Service();
08. 
09.     if (s != null)
10.     {
11.         s.Close();
12.     }
13. }
...

IL:

...
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  2
    .locals init ([0] class Service s, // Create local variable '0' of type Service.
             [1] bool V_1)
    .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
    .line 6,6 : 2,3 ''
    IL_0000:  nop
    .line 7,7 : 3,29 ''
    IL_0001:  newobj     instance void Service::.ctor() // Create new object, reference pushed to stack.
    IL_0006:  stloc.0    // Store to Local: Pop stack into local variable '0'.
    .line 9,9 : 9,23 ''
    IL_0007:  ldloc.0    // Load from Local: Push local variable '0' to stack.
    IL_0008:  ldnull     // Push null to stack.
    IL_0009:  cgt.un     // Pop two items.  If first (reference to service) is greater
                         //   than second (null), then push 1 to stack, otherwise push 0 to stack.
    IL_000b:  stloc.1    // Pop stack into local variable '1'
    .line 16707566,16707566 : 0,0 ''
    IL_000c:  ldloc.1    // Push local variable '1' to stack.
    IL_000d:  brfalse.s  IL_0018  // If top of the stack is 0, then branch to IL_0018

    .line 10,10 : 3,4 ''
    IL_000f:  nop
    .line 11,11 : 10,20 ''
    IL_0010:  ldloc.0    // Load from Local: Push local variable '0' to stack.
    IL_0011:  callvirt   instance void Service::Close()
    IL_0016:  nop
    .line 12,12 : 3,4 ''
    IL_0017:  nop
    .line 13,13 : 2,3 ''
    IL_0018:  ret        // Return.
  } // end of method Program::Main
...

So, even though Roslyn says they are the same, there are some differences at the IL level. I'm no expert at IL, but it looks like null-conditional has some optimizations — it's not actually comparing the object to null, instead branching directly if the object reference ("pointer") is not zero.

Does this mean that Roslyn result from sharplab.io is not correct? Hmm... One more thing to try - created a DLL of the code instead of using the online tools, and used dotPeek to see how it reconstructs the C# code from the compiled DLL, and the result is, as I suspected it, same as what IL is showing, i.e., it knows about the null-conditional operator.

I suppose I could investigate further on how sharplab.io is built or how Roslyn works under the hood, but good enough for now.

One additional note — just because it's doing a null check, it doesn't mean it's completely safe — For some objects, when you call methods such as close(), it will throw an exception if it's disposed already.

No comments:

Post a Comment