public class ClassA<TUid>
{
public TUid? Uid { get; set; }
}
public class ClassB : ClassA<Guid>
{
public void Test()
{
ClassB dto = new ClassB();
dto.Uid = null;
}
}
C# compiler doesn't accept this C# code at the line: dto.Uid = null
saying that
Cannot convert null to 'Guid' because it is a non-nullable value type
Isn't generic type TUid
in ClassA
substituted with Guid
in ClassB
so it should result in Guid?
like this?
public Guid? Uid { get; set; }
If I declare ClassA
as non-generic
public class ClassA
{
public TUid? Uid { get; set; }
}
or pass generic parameter as Guid?
public class ClassB : ClassA<Guid?>
then dto.Uid = null
doesn't cause any problem.
What is it about the parameter type inference rules that confuses the compiler in the first example?
This is because T?
has different meaning depending if T
is a reference type or a value type.
If T
is a value type type, using T?
actually wrap the variable in another struct - the Nullable<T>
struct.
However, if T
is a reference type, using T?
is still the same reference type, only now you're telling the compiler that it can be null.
When the compiler sees T?
and T
is a struct, it lowers the code to Nullable<T>
- however, if T
is a class, it can't do that, because Nullable<T>
can only accept structs as T
.
When you're not using generic constraints, the default behavior is like using a class constraint - so
class C<T>
{
public T? Value {get;set;}
}
is lowered the same as
class C<T> where T : class
{
public T? Value {get;set;}
}
to this:
[NullableContext(2)]
[Nullable(0)]
internal class C<T>
{
[CompilerGenerated]
private T <Value>k__BackingField;
public T Value
{
[CompilerGenerated]
get
{
return <Value>k__BackingField;
}
[CompilerGenerated]
set
{
<Value>k__BackingField = value;
}
}
}
However, when you add the struct
constraint,
this
class C<T> where T : struct
{
public T? Value {get;set;}
}
is lowered to this:
internal class C<T> where T : struct
{
[CompilerGenerated]
private Nullable<T> <Value>k__BackingField;
public Nullable<T> Value
{
[CompilerGenerated]
get
{
return <Value>k__BackingField;
}
[CompilerGenerated]
set
{
<Value>k__BackingField = value;
}
}
}
You can play around with it on SharpLab.IO