Search code examples
c#tostringnullablenull-conditional-operator

Why doesn't Visual Studio warn me about a null reference exception?


I wonder why Visual Studio 2019 doesn’t complain about this piece of C# code:

dynamic deserializedBody = JsonConvert.DeserializeObject(requestBody);
deserializedBody?.date.ToString();

Since deserializedBody?.date could be null, it would mean that the ToString method would be applied to something null. I think something like this:

deserializedBody?.date?.ToString();

would be the correct form to use, but Visual Studio doesn’t complain about the first version. Surely I’m missing something about the true nature of this piece of code.


Solution

  • The null-safe dereferencing operator stops evaluation of the whole expression as soon as it hits null. So if deserializedBody is null, it won't try to evaluate date, and it won't call ToString() on anything - so you won't get an exception.

    Complete example:

    using System;
    
    class Test
    {
        DateTime date = DateTime.UtcNow;
    
        static void Main()
        {
            Test t = null;
            // No exception thrown
            Console.WriteLine(t?.date.ToString());
        }    
    }
    

    Here the expression t?.date.ToString() is equivalent to:

    t is null ? null : t.date.ToString()
    

    (except that t is only evaluated once). It's not equivalent to

    (t is null ? null : t.date).ToString()
    

    ... which is what I suspect you were expecting it to do.

    But no, this doesn't protect against the situation where deserializedBody is non-null, but deserializedBody.date is null.

    If you were expecting a warning due to C# 8 nullable reference types, I believe dynamic expressions are never null-checked. As an example of this, consider:

    class Test
    {
        public string? foo;
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            Test t = new Test();
            dynamic d = t;
    
            Console.WriteLine(t.foo.Length); // Warning 
            Console.WriteLine(d.foo.Length); // No warning
            Console.WriteLine(d.bar); // No warning for this either
        }
    }
    

    I suspect the reason for this is that the compiler basically has no information about what d.foo is. For every case where there's a useful warning, there'd be another case where the warning isn't useful. When you're in dynamic typing territory, you've already accepted a certain degree of risk - it would seem odd to warn that dereferencing d.foo is risky when the access of d.foo is risky to start with.