Thursday, June 14, 2018

Async/await world: casting an async method to Action irrevocably loses the awaitability

My colleague showed me today something that I found interesting. It involves (sometimes unwittingly) casting an awaitable method to Action. In my opinion, the cast itself should now work. After all, an awaitable method is a Func<Task> which should not be castable to Action. Or is it? Let's look at some code:
var method = async () => { await Task.Delay(1000); };
This does not work, as compilation fails with Error CS0815 Cannot assign lambda expression to an implicitly-typed variable, which means we need to set the type explicitly. But what is it? It receives no parameter and returns nothing. So it must be an Action, right? But it is also an async/await method, which means it's a Func<Task>. Let's try something else:
Task.Run(async () => { await Task.Delay(1000); });
This compiles. If we hover or go to implementation for the Task.Run method, we reach the public static Task Run(Func<Task> function); signature. So that does it, right? It IS a Func<Task>! Let's try something else, though.
Action action = async() => { await Task.Delay(1000); };
Task.Run(action);
This compiles again! So it IS an Action, too!

What is my point, though? Consider you would want to create a method that receives an Action as a parameter. You want something done, then to execute the function, something like this:
public void ExecuteWithLog(Action action)
{
    Console.WriteLine("Start");
    action();
    Console.WriteLine("End");
}

And then you want to use it like this:
ExecuteWithLog(async () => {
    Console.WriteLine("Start delay");
    await Task.Delay(1000);
    Console.WriteLine("End delay");
});

The output will be:
Start
Start delay
End
End delay
There is NO WAY of awaiting the original method in the ExecuteWithLog method, as it is received as an Action, and while it waits for a second, execution returns to ExecuteWithLog immediately. Write the method like this:
public async void ExecuteWithLog(Func<Task> action)
{
    Console.WriteLine("Start");
    await action();
    Console.WriteLine("End");
}
and now the output is as expected:
Start
Start delay
End delay
End

Why does this happen? Well, as mentioned above, you start with an Action, then you need to await some method (because now everybody NEEDS to use await/async), and then you get an error that your method is not marked with async. Now it's suddenly something else, not an Action anymore. Perhaps that would be annoying, but this ambiguity in defining what an anonymous async parameterless void method is worse.

0 comments: