Receiving error about nullable type parameter even when parameter has notnull constraint
I think this issue is very similar to what is happening in this post.
Note that a T? where T : class
and a T? where T : struct
are represented very differently in the CLR. The former is just the CLR type T
. There are not separate types in the CLR to differentiate between T
and T?
. T?
in C# just adds extra compile time checking by the C# compiler. On the other hand, The latter is represented by the CLR type Nullable<T>
.
So let's consider your method:
T? Read (Guid id);
How should this be represented in the CLR? What is the return type? The compiler don't know whether T
is a reference type or a value type, so the compiler cannot decide whether the method signature should be:
T Read (Guid id);
or:
Nullable<T> Read (Guid id);
The same error is raised if you don't use the notnull
constraint. You need to specify what that type is with a class
or struct
constraint. You don't need to specify notnull
as structs were always nullable and, with nullable ref types enabled, so are classes.
Just add where T:class
or where T:struct
.
Reference Types
If you add the class
constraint, eg:
#nullable enable
interface IDataAdapter<T>
where T:class
{
T? Read (Guid id); // error CS8627
void Something(T input);
}
class StringAdapter:IDataAdapter<string>
{
public string Read(Guid id)=>id.ToString();
public void Something(string input){}
}
The following call will generate a warning:
var adp=new StringAdapter();
string? x=null;
adp.Something(x); //CS8604: Possible null reference argument ....
Value Types
Using struct
to create an IntAdapter
on the other hand results in a compilation error if the argument is nullable :
interface IDataAdapter<T>
where T:struct
{
T? Read (Guid id); // error CS8627
void Something(T input);
}
class IntAdapter:IDataAdapter<int>
{
public int? Read(Guid id)=>id.ToString().Length;
public void Something(int input){}
}
void Main()
{
var adp=new IntAdapter();
int? x=null;
adp.Something(x); //CS1503: Cannot convert from int? to int
}
That's because the compile generated methods that expect an int?
instead of an int
.
Explanation
The reason is that the compiler has to generate very different code in each case. For a class, it doesn't have to do anything special. For a struct, it has to generate a Nullable< T>.
This is explained in the The issue with T?
section in Try out Nullable Reference Types :
This distinction between nullable value types and nullable reference types comes up in a pattern such as this:
void M<T>(T? t) where T: notnull
This would mean that the parameter is the nullable version of T, and T is constrained to be notnull. If T were a string, then the actual signature of M would be M([NullableAttribute] T t), but if T were an int, then M would be M(Nullable t). These two signatures are fundamentally different, and this difference is not reconcilable.
Because of this issue between the concrete representations of nullable reference types and nullable value types, any use of T? must also require you to constrain the T to be either class or struct.