A problem with Nullable types and Generics in C# 8

T? can only be used when the type parameter is known to be of a reference type or of a value type. Otherwise, we don't know whether to see it as a System.Nullable<T> or as a nullable reference type T.

Instead you can express this scenario in C# 8 by using the [MaybeNull] attribute.

#nullable enable
using System.Diagnostics.CodeAnalysis;

public class C
{
    [return: MaybeNull]
    public T GetDefault<T>()
    {
        return default!; // ! just removes warning
    }
}

This attribute is only included in .NET Core 3.0+, but it is possible to declare and use the attribute internal to your project (although this is not officially supported, there's no reason to assume the behavior will break down the line). To do so, you can just add a namespace+class declaration to your code similar to the following:

namespace System.Diagnostics.CodeAnalysis
{
    /// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
    internal sealed class MaybeNullAttribute : Attribute { }
}

Explanation of the problem

The problem in your first code sample occurs because compiler differently handles nullable value types and nullable reference types:

  • Nullable value type T? is represented by type Nullable<T>.
  • Nullable reference type T? is the same type T but with a compiler-generated attribute annotating it.

Compiler cannot generate code to cover this both cases at the same time, therefore a compilation error occurs. And this error forces us to specify class or struct constraint. This behavior is also stated in the C# specification:

For a type parameter T, T? is only allowed if T is known to be a value type or known to be a reference type.

A good explanation of this problem can be found in this article: Try out Nullable Reference Types. Scroll to the paragraph "The issue with T?".


A workaround to fix the problem

The next workaround can be used if you don't want to create two methods with different names and suppress warnings:

// An overload that will be used by reference types.
public T? GetDefault<T>(T? t = default) where T : class
{
    return default;
}

// An overload that will be used by value types.
public T? GetDefault<T>(T? t = default) where T : struct
{
    return default;
}

Here we added an argument t to the methods GetDefault to make compiler being able to differ these two methods. Now we can use methods GetDefault and compiler will define which overload to use. The drawback of this approach is that GetDefault methods have unusable argument t.


It seems the best solution to this problem will only be in C# 9 as T??

Links:
1. https://github.com/dotnet/csharplang/issues/3471#issuecomment-631722668
2. https://github.com/dotnet/csharplang/issues/3297

At the moment, a working solution was provided by Rikki Gibson. It implies additional code, but it works as it should.