Nullable Generic Typing
Asked Answered
B

2

5

I'm getting some unexpected results when using the nullable reference types feature with generics.

For example, a generic class which has a nullable member

public class Entry<T> where T : notnull
{
    public T? Member { get; set; }
    
}

If I try to assign a nullable int to Member

Entry<int> integers = new();

int? nullNumber = null;

integers.Member = nullNumber;

I get the exception:

Cannot convert source type 'System.Nullable<int>' to target type 'int'

I expect the types of int? of the nullNumber and int? Member of the Entry to be the same.
Why does the compiler think they are different?

Bubb answered 28/5 at 11:56 Comment(0)
F
3

If you have this class declaration:

public class Entry<T> where T: notnull
{
    public T Member { get; set; }   
}

then the generated code will look like this (according to SharpLab):

[NullableContext(1)]
[Nullable(0)]
public class Entry<T>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private T <Member>k__BackingField;

    public T Member
    {
        [CompilerGenerated]
        get
        {
            return <Member>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Member>k__BackingField = value;
        }
    }
}

And when you define your class like below:

public class Entry<T> where T: notnull
{
    public T? Member { get; set; }   
}

then the generated code will look like this:

[NullableContext(2)]
[Nullable(0)]
public class Entry<[Nullable(1)] T>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private T <Member>k__BackingField;

    public T Member
    {
        [CompilerGenerated]
        get
        {
            return <Member>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Member>k__BackingField = value;
        }
    }
}

Your Member's type is T. That means in your code integers.Member is int not int?.


If you want to make your code compile then change the constraint from notnull to struct. With that the Member will be Nullable<T>.

Fenestration answered 28/5 at 12:23 Comment(3)
I assume that if I change the T to inherit from struct I will no longer be able to put objects inside Entry.Bubb
@Bubb Yes that's correct. I'm unaware of any C# feature or attribute which would allow you to declare Entity<int> or Entity<string> and in both cases the Member is nullable.Fenestration
It feels like this feature is incompleteBubb
A
5

In generics, when T is unconstrained with struct or class then T? means "can have its default value".

So in your example, Member is defined as an int that can have its default value (i.e. does not need to be initialised by a constructor or init property).

You would need to declare integers as Entry<int?> integers = new(); to get the behaviour you want.

Also note that you will get the same compile error if you remove where T : notnull.

See here for more details: https://github.com/dotnet/csharplang/discussions/7609

Assemble answered 28/5 at 12:22 Comment(0)
F
3

If you have this class declaration:

public class Entry<T> where T: notnull
{
    public T Member { get; set; }   
}

then the generated code will look like this (according to SharpLab):

[NullableContext(1)]
[Nullable(0)]
public class Entry<T>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private T <Member>k__BackingField;

    public T Member
    {
        [CompilerGenerated]
        get
        {
            return <Member>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Member>k__BackingField = value;
        }
    }
}

And when you define your class like below:

public class Entry<T> where T: notnull
{
    public T? Member { get; set; }   
}

then the generated code will look like this:

[NullableContext(2)]
[Nullable(0)]
public class Entry<[Nullable(1)] T>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private T <Member>k__BackingField;

    public T Member
    {
        [CompilerGenerated]
        get
        {
            return <Member>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Member>k__BackingField = value;
        }
    }
}

Your Member's type is T. That means in your code integers.Member is int not int?.


If you want to make your code compile then change the constraint from notnull to struct. With that the Member will be Nullable<T>.

Fenestration answered 28/5 at 12:23 Comment(3)
I assume that if I change the T to inherit from struct I will no longer be able to put objects inside Entry.Bubb
@Bubb Yes that's correct. I'm unaware of any C# feature or attribute which would allow you to declare Entity<int> or Entity<string> and in both cases the Member is nullable.Fenestration
It feels like this feature is incompleteBubb

© 2022 - 2024 — McMap. All rights reserved.