6

Null propagation is a very nice feature - but where and how does the actual magic happen? Where does frm?.Close() get changed to if(frm != null) frm.Close(); - Does it actually get changed to that kind of code at all?

Dave Gordon
  • 1,815
  • 4
  • 30
  • 52
  • 6
    In the compiler? – Steve Jan 25 '16 at 01:06
  • It's actually more close to `var temp = frm;if(temp != null) temp.Close();` such that it only evaluate only once. See [Docs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-) – joe Jul 23 '19 at 09:39

2 Answers2

20

It is done by the compiler. It doesn't change frm?.Close() to if(frm != null) frm.Close(); in terms of re-writing the source code, but it does emit IL bytecode which checks for null.

Take the following example:

void Main()
{
    Person p = GetPerson();
    p?.DoIt();
}

Compiles to:

IL_0000:  ldarg.0     
IL_0001:  call        UserQuery.GetPerson
IL_0006:  dup         
IL_0007:  brtrue.s    IL_000B
IL_0009:  pop         
IL_000A:  ret         
IL_000B:  call        UserQuery+Person.DoIt
IL_0010:  ret         

Which can be read as:

call - Call GetPerson() - store the result on the stack.
dup - Push the value onto the call stack (again)
brtrue.s - Pop the top value of the stack. If it is true, or not-null (reference type), then branch to IL_000B

If the result is false (that is, the object is null)
pop - Pops the stack (clear out the stack, we no longer need the value of Person)
ret - Returns

If the value is true (that is, the object is not null)
call - Call DoIt() on the top-most value of the stack (currently the result of GetPerson).
ret - Returns

Manual null check:

Person p = GetPerson();
if (p != null)
    p.DoIt();

IL_0000:  ldarg.0     
IL_0001:  call        UserQuery.GetPerson
IL_0006:  stloc.0     // p
IL_0007:  ldloc.0     // p
IL_0008:  brfalse.s   IL_0010
IL_000A:  ldloc.0     // p
IL_000B:  callvirt    UserQuery+Person.DoIt
IL_0010:  ret         

Note that the above is not the same as ?., however the effective outcome of the check is the same.

No null check:

void Main()
{
    Person p = GetPerson();
    p.DoIt();
}

IL_0000:  ldarg.0     
IL_0001:  call        UserQuery.GetPerson
IL_0006:  callvirt    UserQuery+Person.DoIt
IL_000B:  ret         
Rob
  • 26,989
  • 16
  • 82
  • 98
  • 2
    Good answer but it would be nice if you also show the `if (p ! = null)` version and IL. – kjbartel Jan 25 '16 at 01:16
  • Interesting, obviously with manual check he null checks again at call http://stackoverflow.com/questions/193939/call-and-callvirt – Antonín Lejsek Jan 25 '16 at 02:32
  • 1
    Nicely explained... +10 I could see that you use the same LinqPad tool here.. :) – Ian Jan 25 '16 at 03:31
  • Since the Null propagation is not a Syntactical sugar like `Yield`, therefore it can only happen in `IL`, great explanation though. – Mrinal Kamboj Oct 03 '16 at 10:48
2

Does it actually get changed to that kind of code at all?

Well, yes, but at the IL level, not the C# level. The compiler emits IL code that roughly translates to the equivalent C# code you mention.

D Stanley
  • 149,601
  • 11
  • 178
  • 240