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.
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:
- Create a new Unit Test Project in Visual Studio.
- Show the Package Manager Console (from View > Other Windows) and enter
Install-Package Microsoft.CodeAnalysis -Pre
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--;
}
}
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)");
}
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
For (1) and (2), we get the following argument:
ParenthesizedLambdaExpression | () => ...()
ParameterList | ()
InvocationExpression | => ...()
(...) | ...
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
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)