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 ?
CodePudding user response:
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.