6

Consider the following (VS 16.8.0 Preview 2.1 C# 9.0 preview) code:

#nullable enable

using System.Collections.Generic;

class Archive<T> where T : notnull 
{
  readonly Dictionary<string, T> Dict = new();

  public T? GetAt(string key) 
  { 
    return Dict.TryGetValue(key, out var value) ? value : default;
  }
}

class Manager 
{
  public int Age { get; set; }
}

class Main34 
{
  long F3() 
  {
    Archive<long> a = new();
    var johnAge = a.GetAt("john");
    if (johnAge is null) return -1; // Error CS0037  Cannot convert null to 'long' because it is a non - nullable value type
    return johnAge; 
  }

  long F4() 
  {
    Archive<Manager> a = new();
    var johnAge = a.GetAt("john");
    //if (johnAge is null) return -1;
    return johnAge.Age; // Correct ! warning "Derefrencing of a possibly null reference" will be removed if line above unremarked 
  }
}

I am having a hard time understanding/addressing the errors in F3, Seems like the compiler thinks johnAge there is long not long? (as I verified by hovering over it in VS) despite the return of Archive<T>.GetAt being T?

Is there a way to have a generic Archive which will do what I want (a GetAt method that return Nullable even when T is a non nullable basic type ie long) ?

kofifus
  • 17,260
  • 17
  • 99
  • 173
  • 1
    I posted this in your other question, I read through it and it helped me understand a little: https://stackoverflow.com/questions/58229782/receiving-error-about-nullable-type-parameter-even-when-parameter-has-notnull-co All answers are relevant. – Andy Sep 12 '20 at 06:57
  • It's probably because in the `value : default`, the `default` represents 0 (`default(T)`), not null (`default(T?)`). – GSerg Sep 12 '20 at 06:59
  • 2
    It would help if you'd stick to *one* question per post (and ideally format it so we don't need to scroll sideways). Which sample do you actually want an answer to in this post? Please edit the question so it's *only* addressing that. You can always post another question for the other issue. – Jon Skeet Sep 12 '20 at 07:05
  • @JonSkeet ok just F3, edited – kofifus Sep 12 '20 at 07:09
  • Okay, so could you edit the question to *only* have the example you're interested in, and format it so we don't need to scroll horizontally? The clearer your question is, the more likely it is that people will be willing to put effort into answering it. (Personally I'd also format it with a more conventional bracing style - the default in Visual Studio - but that's certainly less important than fixing the long lines.) – Jon Skeet Sep 12 '20 at 07:11
  • I think at the end of the day you have to choose between value-type nullable or reference-type nullable. You can't have one implementation do both as the CLR wouldn't be able to distinguish between the two. – Andy Sep 12 '20 at 07:17
  • Can you not set return type of the method to be string and later on pass it in Long.Parse(), if not null. – Sandeep Pandey Sep 12 '20 at 07:19
  • @Andy, I was hoping with C# 8 non nullable types and C# 9 notnull constraint that could be done as they are very clear hints to the compiler – kofifus Sep 12 '20 at 07:20
  • 2
    @SandeepPandey -- that is what we in the business like to call a *kludge*. – Andy Sep 12 '20 at 07:20
  • @GSerg tried `default(T?)` no go :( – kofifus Sep 12 '20 at 07:27
  • Currently `Archive` doesn't compile for me to start with, so F3 and F4 are somewhat irrelevant. Does `Archive` compile for you? Note that in the IL, any given type reference (e.g. a return type, parameter type or property type) can only be `T` or `Nullable`, not both. Basically reference types and value types have *very* different nullable characteristics. – Jon Skeet Sep 12 '20 at 07:28
  • ok @JonSkeet (note to self don't edit stackoverflow question on your phone) .. I think it's fine now - also updated the sharplab link – kofifus Sep 12 '20 at 07:36
  • No, I still get "error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint." (I believe the SharpLab link is specific to your connection - that gives me an error of "connection lost, reconnecting" so I can't use it to compile) – Jon Skeet Sep 12 '20 at 07:39
  • I *can* compile `Archive` with SharpLab by copying/pasting into a new one though... I don't understand how that would compile. It's possible that I'm using a different preview version to SharpLab. But note how if you compile just `Archive`, the "decompiled" code just has `T` as the return type for `GetAt`, with a nullable annotation. – Jon Skeet Sep 12 '20 at 07:40
  • @JonSkeet hmmm .. my machine using: VS 16.8.0 Preview 2.1 ` net5.0 previewenable` is showing the single error above – kofifus Sep 12 '20 at 07:50
  • @JonSkeet so let's use VS 16.8.0 Preview 2.1 I'll remove the sharplab link alltogether – kofifus Sep 12 '20 at 07:52
  • 2
    Okay, it *is* compiling for me in VS, just not from the command line. The joys of previews. I believe I can add an answer now. – Jon Skeet Sep 12 '20 at 08:07

1 Answers1

8

Fundamentally, this comes down to nullable value types and nullable reference types being very, very different. The CLR is aware of nullable value types, but as far as the CLR is concerned, nullable reference types are just "the normal reference type, with an attribute telling the compiler whether or not it should be considered as nullable".

When T has the notnull constraint, the type T? just compiles to T in the IL. It has to - it can't compile to Nullable<T>, because Nullable<T> constraints T to be a value type.

So for an Archive<long>, the GetAt method will return 0L if the key isn't found in the dictionary - it won't (and can't) return the null value of a Nullable<long>, which is what your code in F3 is effectively expecting.

The whole "nullable reference types" feature suffers from being an attempt to add a "veneer" of nullable awareness over a type system that fundamentally doesn't have it. I'm sure if a new runtime and language were being designed together from scratch now, it would try to unify this more closely. As it is, I believe the feature still has a lot of value - but it definitely makes things really tricky when it comes to generics.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks Jon! Is there a way for me to get what I want ? That is a GetAt method that will return T? So that I will get a warning if I dont check for null, and work the same for Manager and long ? – kofifus Sep 12 '20 at 08:21
  • 2
    @kofifus: No, as described in the second paragraph. The return type can either be `T` or `Nullable` - it can't be both, and the latter isn't valid when `T` is a reference type. It's just one of the annoying sharp corners of nullable reference types, unfortunately. You could potentially create a *subclass* of `Archive` that constrains `T` to be `struct`, and create a new method there... but that would have its own sharp corners. – Jon Skeet Sep 12 '20 at 08:23
  • regarding your explanation above (..When T has the notnull constraint..) just noticed I get the exact same result even if I remove the generic notnull constraint – kofifus Sep 12 '20 at 08:49
  • 2
    @kofifus: The difference is that then you wouldn't be able to create an `Archive` or `Archive`. I'm still surprised that `Archive` compiles at all - either this is a bug in the preview, or it's a change to NRTs in C# 9. – Jon Skeet Sep 12 '20 at 10:14