Search code examples
c#nullablenullable-reference-types

C# compiler does not accept null for a nullable generic type property


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?


Solution

  • 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