Question

Why do we get possible dereference null reference warning, when null reference does not seem to be possible?

Having read this question on HNQ, I went on to read about Nullable Reference Types in C# 8, and made some experiments.

I'm very aware that 9 times out of 10, or even more often, when someone says "I found a compiler bug!" this is actually by design, and their own misunderstanding. And since I started to look into this feature only today, clearly I do not have very good understanding of it. With this out of the way, lets look at this code:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

After reading the documentation I linked to above, I would expect the s == null line to give me a warning—after all s is clearly non-nullable, so comparing it to null does not make sense.

Instead, I'm getting a warning on the next line, and the warning says that s is possible a null reference, even though, for a human, it's obvious it is not.

More over, the warning is not displayed if we do not compare s to null.

I did some Googling and I hit a GitHub issue, which turned out to be about something else entirely, but in the process I had a conversation with a contributor that gave some more insight in this behaviour (e.g. "Null checks are often a useful way of telling the compiler to reset its prior inference about a variable's nullability."). This still left me with the main question unanswered, however.

Rather than creating a new GitHub issue, and potentially taking up the time of the incredibly busy project contributors, I'm putting this out to the community.

Could you please explain me what's going on and why? In particular, why no warnings are generated on the s == null line, and why do we have CS8602 when it does not seem like a null reference is possible here? If nullability inference is not bullet-proof, as the linked GitHub thread suggests, how can it go wrong? What would be some examples of that?

 48  86500  48
1 Jan 1970

Solution

 18

This is effectively a duplicate of the answer that @stuartd linked, so I'm not going to go into super deep details here. But the root of the matter is that this is neither a language bug nor a compiler bug, but it's intended behavior exactly as implemented. We track the null state of a variable. When you initially declare the variable, that state is NotNull because you explicitly initialize it with a value that is not null. But we don't track where that NotNull came from. This, for example, is effectively equivalent code:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

In both cases, you explicitly test s for null. We take this as input to the flow analysis, just as Mads answered in this question: https://stackoverflow.com/a/59328672/2672518. In that answer, the result is that you get a warning on the return. In this case, the answer is that you get a warning that you dereferenced a possibly null reference.

It does not become nullable, simply because we were silly enough to compare it with null.

Yep, it actually does. To the compiler. As humans, we can look at this code and obviously understand that it cannot throw a null reference exception. But the way the nullable flow analysis is implemented in the compiler, it cannot. We did discuss some amount of improvements to this analysis where we add additional states based on where the value came from, but we decided that this added a great deal of complexity to the implementation for not a great deal of gain, because the only places where this would be useful is for cases like this, where the user initializes a variable with a new or a constant value and then checks it for null anyway.

2019-12-31

Solution

 10

If nullability inference is not bullet-proof, [..] how can it go wrong?

I happily adopted the nullable references of C#8 as soon as they were available. As I was used to use the [NotNull] (etc.) notation of ReSharper, I did notice some differences between the two.

The C# compiler can be fooled, but it tends to err on the side of caution (usually, not always).

As a reference for future visitors, these are the scenarios I saw the compiler being pretty confused about (I assume all these cases are by design):

  • Null forgiving null. Often used to avoid the dereference warning, but keeping the object non-nullable. It looks like wanting to keep your foot in two shoes.
    string s = null!; //No warning

  • Surface analysis. In opposition to ReSharper (that does it using code annotation), the C# compiler does still not support a full range of attributes to handle the nullable references.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

It does, though, allow to use some construct to check for nullability that also get rid of the warning:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

or (still pretty cool) attributes (find them all here):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Susceptible code analysis. This is what you brought to light. The compiler has to make assumptions in order to work and sometimes they might seem counter-intuitive (for humans, at least).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Troubles with generics. Asked here and explained very well here (same article as before, paragraph "The issue with T?"). Generics are complicated as they have to make both references and values happy. The main difference is that while string? is just a string, int? becomes a Nullable<int> and forces the compiler to handle them in substantially different ways. Also here, the compiler is choosing the safe path, forcing you to specify what you are expecting:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Solved giving constrains:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

But if we do not use constraints and remove the '?' from Data, we are still able to put null values in it using the 'default' keyword:

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

The last one seems the trickier to me, as it does allow to write unsafe code.

Hope this helps someone.

2019-12-31