3

We have the following method in our code base (.NET Standard 2.0 library):

public Task<T> GetDefaultTask<T>()
{
    return Task.FromResult(default(T));
}

We're currently trying to shift to C# 8.0 Nullability and get a warning in the code above:

warning CS8604: Possible null reference argument for parameter 'result' in 'Task Task.FromResult(T result)'.

Why do we get this warning? To me, it looks perfectly fine, to pass null as the parameter to Task.FromResult.

Important note: We want to allow the Task to contain a null value. But adding Task<T?> would force us to add type constraints which we cannot do.

D.R.
  • 20,268
  • 21
  • 102
  • 205
  • 2
    aren't you declaring an expectation of a non nullable T (Task) and yet have default(T) returning, which is null for reference types? One way would be to change to T? but then you would need to restrict to class. Or us default! instead of default to deny nullability. – Dmitri Apr 26 '20 at 23:32
  • Regardless of whether this could be a good or bad idea, you can get rid of this warning by Enable/Disable commands, like this https://stackoverflow.com/a/61436270/2946329 – Salah Akbari Apr 27 '20 at 00:53
  • @Dmitri: I added an important note to elaborate the situation. – D.R. Apr 27 '20 at 07:03

2 Answers2

2

If T is a non-nullable reference type, null shouldn't be passed to Task.FromResult<T>. The implementation of Task.FromResult doesn't care about null references and you could use Task.FromResult(default(T)!), but then the caller of GetDefaultTask may receive a Task<string> when it should actually be a Task<string?>. Code like GetDefaultTask<string>().Result.Length would compile without warning and cause null reference exceptions at runtime.

As far as I know it's not currently possible to annotate the return type correctly in this situation.

Declaring the method as Task<T?> GetDefaultTask<T>() is not allowed since T could be either a struct or a reference type, and nullable structs and reference types are represented differently.

It is possible to solve this cleanly if T is constrained to be a reference type:

public Task<T?> GetDefaultTask<T>() where T : class

But adding that constraint may cause issues further up the call chain, depending on where that T parameter is coming from.

For similar situations where a generic return value could be either a struct or reference (such as Enumerable.FirstOrDefault) there's the [MaybeNull] attribute, but that can only be applied to the return value itself (the task in this case), not to the generic parameter of the task.

kalimag
  • 1,139
  • 2
  • 6
  • 11
  • I've adjusted my question with an important note. Bottomline: there is no good way to do that? – D.R. Apr 27 '20 at 07:04
  • No, unfortunately not. There is currently no way to declare that an unconstrained generic parameter may be either a nullable reference or a struct. This github issue links to some related discussions and possible future language changes: https://github.com/dotnet/roslyn/issues/29146 – kalimag Apr 27 '20 at 10:08
-1

To me, it looks perfectly fine, to pass null as the parameter to Task.FromResult.

No, that is a bad idea.

If the caller specifies a non-nullable type for T then default(T) can be considered "undefined" (it's actually null, but that's a major shortcoming of C# 8.0's implementation of non-nullable-reference-types (i.e. they can still be null, grrrr). Consider:

// Compiled with C# 8.0's non-nullable reference-types enabled.

Task<String> task = GetDefaultTask<String>();
String result = await task;
Console.WriteLine( result.Length ); // <-- NullReferenceException at runtime even though the C# compiler reported `result` cannot be null.

Avoid using default/default(T) in C# 8.0 for generic types without adequate type-constraints.

There are a few solutions to this problem:

1: Specify a caller-provided default-value:

public Task<T> GetDefaultTask<T>( T defaultValue )
{
    return Task.FromResult( defaultValue );
}

So the call-site needs to be updated and the C# compiler will give a warning or error if the caller tries to use null instead of an exception at runtime:

Task<String> task = GetDefaultTask<String>( defaultValue: null ); // <-- compiler error or warning because `null` cannot be used here.
String result = await task;
Console.WriteLine( result.Length );

2: Add struct vs. class constraints on different methods:

The default(T) of a struct/value-type can be meaningful (or it could be just as dangerous as a null...), as we can safely use default(T) where T : struct but not default(T) where T : class, we can add different overloads for that case:

public Task<T> GetDefaultTask<T>()
    where T : struct
{
    return Task.FromResult( default(T) );
}

public Task<T> GetDefaultTask<T>( T defaultValue )
    where T : class
{
    return Task.FromResult( defaultValue );
}

(Note that you can't overload methods based purely on generic type constraints - you can only overload by generic parameter-count and ordinarily parameter types.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • I've adjusted my question with an important additional note. If I read your post correctly there is no clean way to do that in C#8 as of now? – D.R. Apr 27 '20 at 07:04
  • @D.R. Why can't you add type-constraints? – Dai Apr 27 '20 at 07:33