Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper nullable annotation for async generic method that may return default(T)

I am converting a codebase to C#8 with nullable reference types. I came across the a method similar to the one in this question but async.

public async Task<T> GetAsync<T>()
{
    // sometimes returns default(T); => warning CS8603 Possible null reference return
}

T may be any type, including nullable reference types or nullable value types.

To be clear, I understand WHY this method triggers a warning. What I'd like to know is what annotations can be used to resolve it.

  • I know I can use #nullable disable or default(T)!, but I was hoping for something that's less of a "hammer".
  • I know I can't use [return: MaybNull] because that would apply to the Task itself, not the T.

Is there any other attribute/annotation I can apply to make the compiler happy, or is default(T)! my only option?

like image 812
ChaseMedallion Avatar asked Mar 27 '20 17:03

ChaseMedallion


2 Answers

Form my experience, you can use Task<T?> GetAsync<T>() where T: class to resolve your problem.

like image 159
OsmondJiang Avatar answered Oct 30 '22 07:10

OsmondJiang


In C# 9 we can solve just by adding ?

public async Task<T?> GetAsync<T>()
{
    return default;
}

But you need to distinguish nullable value types and nullable ref types in the calling code. To get a nullable ref type as a return value you can call the method with either <T> or <T?>:

SomeClass? c = await GetAsync<SomeClass>(); // return type is SomeClass?
SomeClass? c2 = await GetAsync<SomeClass?>(); // return type is SomeClass?

To get a nullable value type you need to call it with <T?>:

int? i = await GetAsync<int?>(); // return type is int?
int i2 = await GetAsync<int>(); // return type is int

P.S. I wonder how Microsoft explains us why they can't allow unconstrained T? and then just does this in the next C# version :)

Another option is to use a code from the answer for C# 8.

Answer for C# 8

We can't have async Task<T?> GetAsync<T>() since SomeClass? and SomeStruct? are very different. Also default! is not the best option since we can get nullable reference on non-nullable reference type by calling GetAsync<SomeClass>() in the calling code.

Better option is to have two different methods that use the same private method:

public class Storage
{
    ...
    public Task<T?> GetClassAsync<T>() where T : class
    {
        return GetAsync<T?>();
    }
    public Task<T?> GetStructAsync<T>() where T : struct
    {
        return GetAsync<T?>();
    }
    private async Task<T> GetAsync<T>()
    {
        if (condition)
            return default!;
        string json = await GetJsonAsync();
        T result = JsonSerializer.Deserialize<T>(json);
        return result;
    }
}

And usage:

// return type is SomeClass?
SomeClass? classResult = await storage.GetClassAsync<SomeClass>();

// return type is int?
int? structResult = await storage.GetStructAsync<int>();


// warning: Nullability of type argument doesn't match 'class' constraint
SomeClass? classResult = await storage.GetClassAsync<SomeClass?>();
// error: The type 'int?' must be a non-nullable value type
int? structResult2 = await storage.GetStructAsync<int?>();
like image 42
AlbertK Avatar answered Oct 30 '22 07:10

AlbertK