Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A problem with Nullable types and Generics in C# 8

After adding <Nullable>enable</Nullable> or #nullable enable, I ran into the following problem with my Generic methods:

This does not work:

public T? GetDefault<T>()
{
    return default;
}

enter image description here

This works with warning:

public T GetDefault<T>()
{
   return default;
}

enter image description here

This works individually, but not together.

public T? GetDefault<T>() where T : class
{
    return default;
}

public T? GetDefault<T>() where T : struct
{
    return default;
}

enter image description here

Logically, the first method should work.
What is the correct way (in any framework) out of this situation without creating several methods and suppressing warnings?
[MaybeNull] attribute only works with .Net Core 3.0+.

Also, I asked this questions here

like image 324
Konstantin S. Avatar asked May 20 '20 19:05

Konstantin S.


3 Answers

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 { }
}
like image 166
Rikki Gibson Avatar answered Oct 16 '22 09:10

Rikki Gibson


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.

like image 44
Iliar Turdushev Avatar answered Oct 16 '22 08:10

Iliar Turdushev


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.

like image 5
Konstantin S. Avatar answered Oct 16 '22 07:10

Konstantin S.