Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Annotation for nullable reference with generics

Given the following generic Foo1 function:

struct Key<T> {}
static readonly Key<double> MyKey = new Key<double>();
T? Foo1<T>(Key<T> key)
{
    return default;
}

A naive reader would assume that:

var foo1 = Foo1(MyKey);

foo1 is of type double?, it turns out that the compiler is picking double for the return type. I need to explictly add a constraint to get a nullable return value:

T? Foo2<T>(Key<T> key) where T : struct // really return a nullable
{
    return default;
}

Could someone explain why the annotation for nullable reference ? is not being picked up in my first Foo1 function ?

like image 616
malat Avatar asked Oct 15 '22 20:10

malat


1 Answers

Let's start with some background:

Before C# 9.0 Foo1 was invalid. Even in C# 8.0 with enabled nullable references:

CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type

Foo2 was valid even before C# 8.0 because T? made sense only if T was a struct, and in this case T? had a different type from T (Nullable<T>). So far, it's quite simple.

Starting with C# 8.0 nullable references have been introduced, which caused some confusion. From now on T? can either mean Nullable<T> or just T. This version didn't allow T? without a constraint but it allowed also when you specified where T : class.

Without using constraints you had to use attributes to indicate that T can be null as a return value:

// C# 8.0: Poor man's T?
[return: MaybeNull] T Foo1<T>(Key<T> key) => default;

And what if T is a value type now? It clearly will not change its type to Nullable<T> in the return value. To return a double? your type argument must also be double?, meaning, MyKey must also be a Key<double?>.

In C# 9.0 the restriction for T? has been relaxed, now it does not need a constraint:

// C# 9.0: this is valid now
T? Foo1<T>(Key<T> key) => default;

But it essentially now means the same as the C# 8.0 version. Without the where T : struct constraint T? is the same type as T so it is nothing but an indication that the result can be null, which can appear in compiler warnings. To return nullable value types you must use double? as a generic argument, which also mean that your Key also must have a nullable type defined:

static readonly Key<double?> MyKey = new Key<double?>();

If a nullable key makes no sense in your case, then you cannot do anything but specifying where T : struct constraint as in Foo2 so the old rule kicks in: T? and T have different types where T? means Nullable<T>.


Update: The main difference between Foo1 and Foo2 is maybe more obvious if you see their decompiled source:

[System.Runtime.CompilerServices.NullableContext(2)]
private static T Foo1<T>([System.Runtime.CompilerServices.Nullable(new byte[] {
    0,
    1
})] Key<T> key)
{
    return default(T);
}

private static Nullable<T> Foo2<T>(Key<T> key) where T : struct
{
    return null;
}

Note that the return type of Foo1 is simply T with some annotation so the compiler can emit the proper warnings.

like image 76
György Kőszeg Avatar answered Oct 18 '22 15:10

György Kőszeg