Explicitly use a Func<Task> for asynchronous lambda function when Action overload is available

68,900

Solution 1

Because Task.Run has signatures of both Task.Run(Func<Task>) and Task.Run(Action), what type is the async anonymous function compiled to? An async void or a Func<Task>? My gut feeling says it will compile down to an async void purely because its a non-generic type however the C# Compiler might be smart and give Func<Task> types preference.

The general rule, even without async, is that a delegate with a return type is a better match than a delegate without a return type. Another example of this is:

static void Foo(Action a) { }
static void Foo(Func<int> f) { }
static void Bar()
{
  Foo(() => { throw new Exception(); });
}

This is unambiguous and calls the second overload of Foo.

Also, is there a way to explicitly declare which overload I wish to use?

A nice way to make this clear is to specify the parameter name. The parameter names for the Action and Func<Task> overloads are different.

Task.Run(action: async () => {
  await Task.Delay(1000);
});
Task.Run(function: async () => {
  await Task.Delay(1000);
});

Solution 2

I just checked it gets compiled into Task.Run(Func<Task>) by default, I don't have good explanation for this.

Here is the relevant part of IL

IL_0001:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0006:  brtrue.s    IL_001B
IL_0008:  ldnull      
IL_0009:  ldftn       UserQuery.<Main>b__0
IL_000F:  newobj      System.Func<System.Threading.Tasks.Task>..ctor//<--Note here
IL_0014:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0019:  br.s        IL_001B
IL_001B:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0020:  call        System.Threading.Tasks.Task.Run

you can check this easily using visual studio type inference, it will show you what method it will be compiled if you just place mouse over the method, or just click the method press F12 you can see the metadata which will tell you what was the type inferred by compiler.

Also, is there a way to explicitly declare which overload I wish to use? Yes, Specify the delegate explicitly.

Task.Run(new Action(async () =>
{
    await Task.Delay(1000);
}));

Task.Run(new Func<Task>(async () =>
{
    await Task.Delay(1000);
}));
Share:
68,900

Related videos on Youtube

jduncanator
Author by

jduncanator

Updated on June 27, 2020

Comments

  • jduncanator
    jduncanator almost 4 years

    Reading over this blog post on some of the gotchas of C#5's async/await. It mentions in Gotcha #4 something that is quite profound and that I hadn't thought of before.

    Briefly, it covers the scenario where you have a method that has two overloads, one that takes an Action and one that takes a Func<Task> (for example Task.Run). This issue is rooted in the argument that async void methods should only be used for event handlers, with the post then going on to portray the following scenario - What does the compiler infer when a lambda function like the following can be compiled to both a Func<Task> and an Action:

    Task.Run(async () => {
      await Task.Delay(1000);
    });
    

    Because Task.Run has signatures of both Task.Run(Func<Task>) and Task.Run(Action), what type is the async anonymous function compiled to? An async void or a Func<Task>? My gut feeling says it will compile down to an async void purely because its a non-generic type however the C# Compiler might be smart and give Func<Task> types preference.

    Also, is there a way to explicitly declare which overload I wish to use? I know I could just create a new instance of Func<Task> and pass in the async lambda function there but it would still compile down to a async void and then pass that into the constructor of the Func<Task>. What is the ideal way to make sure its compiled as a Func<Task>?

    • Stephen Cleary
      Stephen Cleary over 10 years
      Microsoft did actually change the compiler so that it would choose Func<Task> over Action, all else being equal. AFAIK, Eric Lippert has not yet addressed this on his blog... :)
    • horgh
      horgh over 6 years
  • jduncanator
    jduncanator over 10 years
    If you read the next sentence, this only "sorta" mitigates the issue... the inside async () => lambda function still compiles to an async void delegate.
  • Sriram Sakthivel
    Sriram Sakthivel over 10 years
    Does my edit makes sense? Or am missing something..?
  • jduncanator
    jduncanator over 10 years
    Yes! Your edit makes perfect sense! I was Ctrl-Shift-Spaceing in the method parameters and it was shown the Action overloads signature so I thought it was compiling to an async void. Turns out its not! The compiler automatically picks Func<Task> for async lambdas as preference over Action! Thanks!
  • Jeppe Stig Nielsen
    Jeppe Stig Nielsen over 10 years
    Really cool to use parameter name to choose overload. Otherwise, one would cast the entire lambda to the desired delegate type, but that looks ugly. In your first example, the name of the parameter f is a little confusing (though legal) since the method itself is also called f.
  • jduncanator
    jduncanator over 10 years
    Awesome! Is it standard to use different parameter names in the .NET Framework?
  • Admin
    Admin over 10 years
    @jduncanator I'm not sure how to read that. The parameter names are considered part of the API, and changes to the parameter names are considered breaking changes, so it's "standard" in that you can trust that MS won't suddenly change it without a very good reason. Does that answer your question?