45

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?

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Andrew Savinykh
  • 25,351
  • 17
  • 103
  • 158
  • It seems that the compiler itself sets behavior that at this point the variable "s" could be null. Anyway, if I ever use strings or objects then there should be always a check before you call a function. "s?.Length" should do the trick and the warning itself should dissapear. – chg Dec 30 '19 at 01:07
  • 1
    @chg, there should be no need for `?` because `s` is not nullable. It does not become nullable, simply because we were silly enough to compare it with `null`. – Andrew Savinykh Dec 30 '19 at 01:09
  • I was following an earlier question (sorry, can't find it) where it was posited that if you add a check that a value is null, then the compiler takes that as 'hint' that the value _might_ be null, even if that is demonstrably not the case. – stuartd Dec 30 '19 at 01:10
  • 1
    @stuartd, yep, that's what it seems it is. So now the question is: why is this useful? – Andrew Savinykh Dec 30 '19 at 01:10
  • OK, found it - [Why does this code give a “Possible null reference return” compiler warning?](https://stackoverflow.com/questions/59306751/why-does-this-code-give-a-possible-null-reference-return-compiler-warning) – stuartd Dec 30 '19 at 01:15
  • @stuartd, it's relevant, thank you for the link, but this is about a different warning. So most of the reasoning in the answers apply to that warning and not to the one in my question. – Andrew Savinykh Dec 30 '19 at 01:19
  • _this is about a different warning_ - I suggest you re-read the answers to that question and [this comment](https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947) to see why they are the same issue - the compiler does a [path-independent lattice flow check for nullable values](https://twitter.com/andygocke/status/1206692183111163904) - and because it is path-independent, it generates this spurious warning. – stuartd Dec 30 '19 at 01:24
  • @AndrewSavinykh But variable `s` is a string which is a reference type and always is nullable. To specify type nullable is necessary only for value types – chg Dec 30 '19 at 01:27
  • @chg, no `s` is of a non-nullable reference type. – Andrew Savinykh Dec 30 '19 at 01:30
  • @AndrewSavinykh then your second line should have a warning that you are trying to compare a non-nullable with a null. – chg Dec 30 '19 at 01:35
  • 1
    @chg, well, that's what I say in the question body, is not it? – Andrew Savinykh Dec 30 '19 at 01:36
  • @stuartd, I would like to thank you again for all these links, they are very relevant and helpful. However, I still think that this question is not a duplicate, for the reason above and also because it is not clear how path-independent lattice flow check precludes using the compiler's type knowledge of a specific var. (It does explain the problem with XOR condition mentioned though). – Andrew Savinykh Dec 30 '19 at 01:47
  • @stuartd similarly it does not address why the `s == null` line does NOT produce a warning. – Andrew Savinykh Dec 30 '19 at 01:48
  • Which tooling and versions are you using? I don't get the same warning [here](https://sharplab.io/#v2:CYLg1APgAgTAjAWAFCwARTgdmQb2ag9OANnQBZUBZAQwEsA7ACgEp9C8lCvUA3agJ1QBnVAF5UAIgkBuNt14DUAIzHCx4+gFcANtumoA9AdQBJAGaoAngHtNqAMbWAtk4Cm9AC6oPAC1ojtBldUWw8AGm8fYIB3AXoGAHMQ+kjgwPpgpVdta2jUYH9qAAci1wEhOW4+QVpVIQA6ABl3BN99I1RY/nj6JIBhAGUADmIABhgQVAARV35XM1n3e2DrC2pUIushIVolbUtULV1UOYW5+mXKgF9kK6A==) – Paulo Morgado Dec 30 '19 at 07:33
  • @PauloMorgado .net core 3.1. Also just to make sure, we are on the same page, these are compile-time warnings. And also your code is missing [`#nullable enable`](https://sharplab.io/#v2:CYLg1APgxAdgrgGwQQwEYIKYAIMzZgWACgABAJgEZjysSKB2Ygb2KzdooDZaAWLAWWQBLGAAoAlK3Ysi7OVgBuyAE5YAzlgC8WAEQ6A3FPmKVWVFvVbt8JPqwB6e1gCSAMywBPAPZwsAYy8AW0DcABcsUIALIQ0EEWwfUIAaCMjsAHcVGBEAcywvGFTsOJhsVAwEL3SsYBjkAAd6jBU1I3klVSELNQA6ABlcHKi7RyxM5WyYPIBhAGUADk4ABjIQLAARDGUMVy3cPwT3ZCx6rzU1IXQPLBsELG3d7ZgDtoBfYlegA===) – Andrew Savinykh Dec 30 '19 at 08:06
  • Sorry! My bad! Here is the [correct link](https://sharplab.io/#v2:CYLg1APgAgTAjAWAFCwARTgdmQb2QYgDsBXAG1IEMAjUgU1VsOruVTfTgDZ0AWVAWQoBLQgAoAlK3Z4k7OagBuFAE6oAzqgC8qAEQ6A3FPmKVqKlvVbtJcvtQB6e6gCSAM1QBPAPbFUAYy8AW0DGABdUUIALIQ1SEXofUIAaCMj6AHcVQhEAc1QvQlT6OMJ6KlpSL3TUYBiKAAd62hU1I3klVSELNQA6ABlGHKi7R1RM5WzCPIBhAGUADk4ABhgQVAARWmVaVy3GPwT3ClR6rzU1IRoPVBtSVG3d7cIDtoBfZFegA===). And it does repro the issue. Have you tried reporting at https://github.com/dotnet/csharplang/issues ? – Paulo Morgado Dec 30 '19 at 11:47
  • @PauloMorgado I'd like to triage it here first, I'm not convinced that this is the issue with the langugage, also this repo that you linked is not apropriate for this type of issues, becaus if anything this would be an issue with a compiler, not with language design. – Andrew Savinykh Dec 30 '19 at 19:18
  • @PauloMorgado pleae do not remove my tags. – Andrew Savinykh Dec 30 '19 at 19:18

2 Answers2

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.

333fred
  • 181
  • 1
  • 5
  • Thank you. This mostly addresses similarities with the other question, I'd like to address differences more. For example, why `s == null` does not produce a warning? – Andrew Savinykh Dec 31 '19 at 05:37
  • Also I just realized that with `#nullable enable`; `string s = "";s = null;` compiles and works (it still does produce a warning) what are benefits, of the implementation that allows assigning null to a "non-nullable reference" in enabled null annotation context? – Andrew Savinykh Dec 31 '19 at 05:40
  • Mad's answer focuse on the fact that "[compiler] does not track the relationship between the state of separate variables" we do not have separate variables in this example, so I have trouble applying the rest of Mad's answer to this case. – Andrew Savinykh Dec 31 '19 at 05:42
  • It goes without saying, but remeber, I'm here not to critique, but to learn. I've been using C# from the time it first came out in 2001. Even though I'm not new to the language, the way the compiler behaves was surprising to me. The goal of this question is to lean the rationale why this behaviour is useful for humans. – Andrew Savinykh Dec 31 '19 at 05:47
  • There are valid reasons to check `s == null`. Perhaps, for example, you're in a public method, and you want to do parameter validation. Or, perhaps you're using a library that annotated incorrectly, and until they fix that bug you have to deal with null where it wasn't declared. In either of those cases, if we warned, it would be a bad experience. As for allowing assignment: local variable annotations are just for reading. They don't affect the runtime at all. In fact, we put all those warnings in a single error code so that you can turn them off if you want to to reduce code churn. – 333fred Dec 31 '19 at 23:09
8

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.

Alvin Sartor
  • 2,249
  • 4
  • 20
  • 36
  • 1
    I'd suggest giving the docs on nullable attributes a read: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-attributes. They'll solve some of your issues, particularly with the surface analysis section and generics. – 333fred Dec 31 '19 at 23:17
  • Thanks for the link @333fred. Even though there are attributes you can play with, an attribute that solve the problem I posted (something that tells me that `ThrowIfNull(s);` is assuring me that `s` is not null), doesn't exist. Also the article explains how to handler *non-nullable* generics, while I was showing how you can "fool" the compiler, having a null value but no warning about it. – Alvin Sartor Jan 02 '20 at 09:08
  • Actually, the attribute does exist. I filed a bug on the docs to add it. You're looking for `DoesNotReturnIf(bool)`. – 333fred Jan 07 '20 at 23:31
  • @333fred actually I'm looking for something more like `DoesNotReturnIfNull(nullable)`. – Alvin Sartor Jan 08 '20 at 07:57
  • 1
    "Hope this helps someone" – it does ;) also coming from Resharper's attributes. I also had to change some code because the C# 8 attributes can't cover all cases yet (easiest example is that while there is a `NotNullIfNotNull` there is no `NullIfNotNull`, or other variations of it). – enzi Oct 16 '20 at 15:07
  • @AlvinSartor wouldn't `DoesNotReturnIf(xyz==null)` be the same as `DoesNotReturnIfNull(xyz)`? – GaTechThomas Nov 25 '20 at 20:19