Null propagation is a very nice feature - but where and how does the actual magic happen?
Where does frm?.Close()
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.
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