Joerg
Joerg

Reputation: 924

Why can an async method with return type ValueTask await a regular Task without generating a compiler error?

The following code does not compile

public ValueTask Foo()
{
    return Task.Delay(1000);
}

but yields an Error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask as expected.

However, this

public async ValueTask Bar()
{
    await Task.Delay(1000);
}

does compile fine.

I was just wondering how this works. Is this all down to compiler magic and its async-await syntactic sugar or is there something else going on?

For context: I came across this when implementing IAsyncDisposable.DisposeAsync().

Upvotes: 3

Views: 2699

Answers (3)

Pavel Mayorov
Pavel Mayorov

Reputation: 396

Inside async method, you must return inner type of task, but you cannot return a task itself:

public async Task<int> Foo()
{
    return 42; // that works
}

public async Task<int> Foo()
{
    return Task.FromResult(42); // error
}

Look at the first example again: you don't need any task at all to return something from async method. async keyword acts as some sort of wrap operator, that transforms any T type to Task<T>. And it can work with any task-like type.

On the other hand, await operator acts like unwrap operator that transforms any Task<T> type into inner type T:

public async void Foo()
{
    int x = await Task.FromResult(42); // that works
}

public async void Foo()
{
    int x = Task.FromResult(42); // error
}

public async void Foo()
{
    int x = await 42; // error too
}

And await operator can work with any awaitable type, such as Task, ValueTask, ConfiguredTaskAwaitable, even YieldAwaitable

And this two operators can be combined in any way.

Let's look again in your code:

public async ValueTask Bar()
{
    await Task.Delay(1000);
}

First, await transforms Task into void. Next, async transforms implicit void into ValueTask. There are nothing to wonder here.

Upvotes: 1

Paulo Morgado
Paulo Morgado

Reputation: 14856

Title doesn't match what's being asked. In fact, the question proves it's possible.

If you want to return ValueTask that represents a Task whithout having a method turned into a state machine, you can:

public ValueTask Foo()
{
    return new ValueTask(Task.Delay(1000));
}

Upvotes: 2

Sweeper
Sweeper

Reputation: 273540

Is this all down to compiler magic and its async-await syntactic sugar?

In short, yes. Whenever you await, the compiler needs to generate a state machine for that method. The task returned from the method then, is one that "represents" the state machine, rather than the single task that you are awaiting.

As a result, it doesn't matter what tasks you are awaiting anymore. The compiler just has to build the state machine according to where your awaits are in your method, and then build a new task.

Compare the code generated from the following snippets on SharpLab:

1:

async Task Bar()
{
    await Task.Delay(1000);
}

2:

async ValueTask Bar()
{
    await Task.Delay(1000);
}

The only substantial difference is that one uses AsyncTaskMethodBuilder to build the task being returned, and the other using AsyncValueTaskMethodBuilder.

For more details about the difference of awaiting a task vs directly returning the task, see this chain of duplicates.

Upvotes: 4

Related Questions