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?>();