This is done by the compiler. It does not change frm?.Close()to if(frm != null) frm.Close();in terms of rewriting the source code, but it emits an IL bytecode that checks for null.
Take the following example:
void Main()
{
Person p = GetPerson();
p?.DoIt();
}
Compiles:
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
What can be read as:
call - GetPerson() - .
dup - ()
brtrue.s - . , - ( ), IL_000B
( null)
pop - ( , Person)
ret -
(.. )
call - DoIt() ( GetPerson).
ret -
:
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
, ?., .
:
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