Search code examples
c#nullpropagation

C# Null propagation - Where does the magic happen?


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?


Solution

  • 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