Task.Run and expected delegate

2019-02-20 14:18发布

问题:

I am not sure how to make sense out of the following observed results.

var f = new Func<CancellationToken,string>(uc.ViewModel.SlowProcess);

1) (VALID) string dataPromise = await Task.Run<string>(() => f(token), token);

2) (VALID) string dataPromise = await Task.Run<string>(() => uc.ViewModel.SlowProcess(token), token);

3) (ERROR) string dataPromise = await Task.Run<string>(f(token), token);

uc.ViewModel.SlowProcess is a method that takes a CancellationToken as a parameter and returns a string.

Item 1) and 2) are valid and work correctly. Item 3) is invalid, giving the follow errors:

Error 1 The best overloaded method match for 'System.Threading.Tasks.Task.Run(System.Func>, System.Threading.CancellationToken)' has some invalid arguments

Error 2 Argument 1: cannot convert from 'string' to 'System.Func>'

Why can't I pass f(token) as a delegate? If I do it with a method that takes no parameters, it also works.

回答1:

Passing f(token) as a delegate is actually what you're doing in (1).

() => f(token) is a delegate with no arguments and return type string.

f(token) is not a delegate, but an immediate invocation of method f that returns a string. That means, your code isn't called by the Task infrastructure, but by yourself, before the Task is even created, resulting in a string. You can't create a Task from that string, which leads to the syntax error.

I would stick with what you did in (1).


Edit: Let's clarify things a bit.

IL code probably shows all.

Probably, but we should rather try to understand what the code actually means. We can do this using Roslyn, the .NET Compiler Platform:

  1. Create a new Unit Test Project in Visual Studio.
  2. Show the Package Manager Console (from View > Other Windows) and enter Install-Package Microsoft.CodeAnalysis -Pre
  3. Create a new class containing the following code:

    using System;
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.CSharp;
    
    public class SyntaxTreeWriter : CSharpSyntaxWalker
    {
        public static void Write(string code)
        {
            var options = new CSharpParseOptions(kind: SourceCodeKind.Script);
            var syntaxTree = CSharpSyntaxTree.ParseText(code, options);
            new SyntaxTreeWriter().Visit(syntaxTree.GetRoot());
        }
    
        private static int Indent = 0;
        public override void Visit(SyntaxNode node)
        {
            Indent++;
            var indents = new String(' ', Indent * 2);
            Console.WriteLine(indents + node.CSharpKind());
            base.Visit(node);
            Indent--;
        }
    }
    
  4. Now, let's create a Test Class and analyze your statements from above:

    [TestMethod]
    public void Statement_1()
    {
        SyntaxTreeWriter.Write("Task.Run<string>(() => f(token), token)");
    }
    
    [TestMethod]
    public void Statement_2()
    {
        SyntaxTreeWriter.Write("Task.Run<string>(() => uc.ViewModel.SlowProcess(token), token)");
    }
    
    [TestMethod]
    public void Statement_3()
    {
        SyntaxTreeWriter.Write("Task.Run<string>(f(token), token)");
    }
    
  5. For each case, we get some common output:

    (...)
      InvocationExpression             | Task.Run<string>(..., token)
        SimpleMemberAccessExpression   | Task.Run<string>
          IdentifierName               | Task
          GenericName                  |      Run<string>
            TypeArgumentList           |         <string>
              PredefinedType           |          string
        ArgumentList                   |                 (..., token)
          Argument                     |                  ...
            (...)                      |                  ...
          Argument                     |                       token
            IdentifierName             |                       token
    
  6. For (1) and (2), we get the following argument:

    ParenthesizedLambdaExpression      | () => ...()
      ParameterList                    | ()
      InvocationExpression             |    => ...()
        (...)                          |       ...
    
  7. For (3) instead, we get the following argument:

    InvocationExpression               | f(token)
      IdentifierName                   | f
      ArgumentList                     |  (token)
        Argument                       |   token
          IdentifierName               |   token
    

Ok, what do we have here?

A ParenthesizedLambdaExpression obviously is an inline method declaration. The type of this expression is determined by the parameter list (input), the type of the lambda body (output) and by the expected type where the lambda is used (type inference).

What does that mean?

  • Our lambdas in (1) and (2) have an empty parameter list and thereby no input.
  • In both lambdas, we invoke something (method or delegate) that returns a string.
  • That means, the type of our lambda expression will be one of the following:
    • Func<string>
    • Expression<Func<string>>
    • Action
    • Expression<Action>
  • The type of the first argument of our Task.Run method determines which type is used. We have the following possibilities, given that we use an overload that takes a CancellationToken as second parameter:
    • Action
    • Func<TResult>
    • Func<Task>
    • Func<Task<TResult>>
  • There are two matching types:
    • Func<string>, where TResult is string
    • Action
  • The first one has higher precedence, so we use the overload Task.Run<string>(Func<string>, CancellationToken)

Okay. That's why (1) and (2) both work: They use a lambda, which in fact generates a delegate, and the type of the delegate matches the expectations of the Task.Run method.

Why is f(token) not working then?

Once you accept that passing a parameterized delegate essentially gets treated like passing the function(s) it wraps, everything works like you would expect.

There is no such thing as a "parameterized delegate". There are delegates that have parameters (Action<T>, Func<T,TResult>...) but this is fundamentally different from f(token) which is an invocation of delegate f, which results in the return value of the delegated method. That's why the type of f(token) simply is string:

  • The type of an InvocationExpression is the return type of the called method. The same applies to delegates.
  • The type of f(token) is string, because f has been declared as Func<CancellationToken,string>.
  • Our overloads for Task.Run still take:
    • Action
    • Func<TResult>
    • Func<Task>
    • Func<Task<TResult>>
  • There is no match. Code's not compiling.

How could we make it work?

public static class TaskExtensions
{
    public static Task<TResult> Run<TResult>(Func<CancellationToken, TResult> function, CancellationToken token)
    {
        Func<TResult> wrappedFunction = () => function(token);
        return Task.Run(wrappedFunction, token);
    }
}

This could be called like TaskExtensions.Run(f, token). But I would not recommend doing that, as it provides no additional value whatsoever.

Additional information:

EBNF Syntax: C# 1.0/2.0/3.0/4.0
C# Language Specification



回答2:

Your delegate that you are passing into Task.Run does not match and any of the expected signatures. It takes in a CancellationToken and returns a string which does not match any of the allowed signatures.. Getting rid of the cancellation token allows it to match on of these:

Run<TResult>(Func<TResult>)  
Run<TResult>(Func<TResult>, CancellationToken)