Search code examples
c#nulloperatorsdereferencenull-coalescing-operator

Is the Elvis Operator (Nullsave Dereference Operator) causing null reference exceptions?


When calling an extension method on an expression containing an elvis operator (i.e. the nullsafe dereferencing operator; ?.) the resulting null is not passed to the extension method as expected. In essence, it might cause unexpected null reference exceptions. Here's a program demonstrating this:

class Program
{
    static void Main(string[] args)
    {
        string nil = null;
        foreach (var c in ((nil?.ToCharArray()).EmptyIfDefault())) { }; // works
        foreach (var c in (nil?.ToCharArray().EmptyIfDefault())) { }; // nullref
    }
}

public static class Utility
{
    public static char[] EmptyIfDefault(this char[] target)
    {
        return target ?? new char[0];
    }
}

Does anybody if this behavior is by design? Notice there's no ? between the ToCharArray() and the EmptyIfDefault. If there was, I'd understand the current behavior. Right now, it seems like a bug. (What's the correct way to report this to Microsoft?)

For others who see the same behavior: the extra braces seem to prevent it.

(By the way: here's the actual EmptyIfNull I'm using:)

    public static IEnumerable<TTarget> EmptyIfNull<TTarget>(this IEnumerable<TTarget> target)
    {
        return target ?? Enumerable.Empty<TTarget>();
    }

edit I'll just include the answer given below into my question:

It is related to a common pitfall:

var txt = "I am " +
    age>=18 ? "mature" : "not old" +
    " enough.";

This is also interpreted as

var txt = "I am " + 
    age >= 18 
        ? "mature" 
        : ("not old" + " enough.");

When rewritten, The behavior without braces makes sense:

foreach(var c in 
    nil == null
        ? null
        : nil.ToCharArray().EmptyIfDefault()) { }; // nullref

Solution

  • While it's unintuitive at first, it definitely is not a bug. You're getting a NullReferenceException because you're trying to iterate over null (it's not throwing the exception while evaluating the expressions)

    Let's look at this example:

    var t = nil?.ToCharArray().EmptyIfNull();
    

    The above will not call EmptyIfNull, because nil will be null and the method chain will short-circuit to return null.

    That is, we can write the above as:

    IEnumerable<char> t;
    if (nil != null)
        t = nil.ToCharArray().EmptyIfNull();
    else
        t = null;
    

    Note that the EmptyIfNull only executes if the initial condition passes (that is, nil is not null).

    Now, why do brackets fix it?

    var t = (nil?.ToCharArray()).EmptyIfNull();
    

    This can be re-written as:

    IEnumerable<char> t;
    IEnumerable<char> temp;
    if (nil != null)
        temp = nil.ToCharArray();
    else
        temp = null;
    t = temp.EmptyIfNull();
    

    See that the short-circuiting behavior only applies to the inner expression - and then we always call EmptyIfNull on the result.