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:
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:
And then you want to use it like this:
The output will be:
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.
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); };This compiles again! So it IS an Action, too!
Task.Run(action);
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:
StartThere 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:
Start delay
End
End delay
public async void ExecuteWithLog(Func<Task> action)and now the output is as expected:
{
Console.WriteLine("Start");
await action();
Console.WriteLine("End");
}
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.
Comments
Very nice connection! Yes, it is related to the void async method issue. Yet, in the post I described a way to get to an async method returning void without actually meaning to: you just wanted a simple action that happened to execute some async code and you just fixed the code warnings. I think a rule that warns of void asyncs is a good thing, as a way of letting people know they got into this mess.
SideriteAvoid Async Void - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
Alin