Reputation: 498
Is this possible in C#? The following code produces a compiler error.
HashSet<Task<(string Value, int ToNodeId)>> regionTasks =
new HashSet<Task<(string Value, int ToNodeId)>>();
foreach (Connection connection in Connections[RegionName])
{
regionTasks.Add(async () =>
{
string value = await connection.GetValueAsync(Key);
return (value, connection.ToNode.Id);
}());
}
The C# compiler complains, "Error CS0149: Method name expected." It's unable to infer the lambda method's return type.
Note my technique of invoking the lambda method immediately via the () after the the lambda block is closed {}. This ensures a Task
is returned, not a Func
.
The VB.NET compiler understands this syntax. I am stunned to find an example of the VB.NET compiler outsmarting the C# compiler. See my An Async Lambda Compiler Error Where VB Outsmarts C# blog post for the full story.
Dim regionTasks = New HashSet(Of Task(Of (Value As String, ToNodeId As Integer)))
For Each connection In Connections(RegionName)
regionTasks.Add(Async Function()
Dim value = Await connection.GetValueAsync(Key)
Return (value, connection.ToNode.Id)
End Function())
Next
The VB.NET compiler understands the End Function()
technique. It correctly infers the lambda method's return type is Function() As Task(Of (Value As String, ToNodeId As Integer))
and therefore invoking it returns a Task(Of (Value As String, ToNodeId As Integer))
. This is assignable to the regionTasks
variable.
C# requires me to cast the lambda method's return value as a Func
, which produces horribly illegible code.
regionTasks.Add(((Func<Task<(string Values, int ToNodeId)>>)(async () =>
{
string value = await connection.GetValueAsync(Key);
return (value, connection.ToNode.Id);
}))());
Terrible. Too many parentheses! The best I can do in C# is explicitly declare a Func
, then invoke it immediately.
Func<Task<(string Value, int ToNodeId)>> getValueAndToNodeIdAsync = async () =>
{
string value = await connection.GetValueAsync(Key);
return (value, connection.ToNode.Id);
};
regionTasks.Add(getValueAndToNodeIdAsync());
Has anyone found a more elegant solution?
Upvotes: 5
Views: 1860
Reputation: 43475
An easy way to invoke asynchronous lambdas in order to get the materialized tasks, is to use a helper function like the Run
below:
public static Task Run(Func<Task> action) => action();
public static Task<TResult> Run<TResult>(Func<Task<TResult>> action) => action();
Usage example:
regionTasks.Add(Run(async () =>
{
string value = await connection.GetValueAsync(Key);
return (value, connection.ToNode.Id);
}));
The Run
is similar with the Task.Run
, with the difference that the action
is invoked synchronously on the current thread, instead of being offloaded to the ThreadPool
. Another difference is that an exception thrown directly by the action
will be rethrown synchronously, instead of being wrapped in the resulting Task
. It is assumed that the action
will be an inline async
lambda, as in the above usage example, which does this wrapping anyway, so adding another wrapper would be overkill. In case you want to eliminate this difference, and make it more similar with the Task.Run
, you could use the Task
constructor, and the RunSynchronously
and Unwrap
methods, as shown below:
public static Task Run(Func<Task> action)
{
Task<Task> taskTask = new(action, TaskCreationOptions.DenyChildAttach);
taskTask.RunSynchronously(TaskScheduler.Default);
return taskTask.Unwrap();
}
public static Task<TResult> Run<TResult>(Func<Task<TResult>> action)
{
Task<Task<TResult>> taskTask = new(action, TaskCreationOptions.DenyChildAttach);
taskTask.RunSynchronously(TaskScheduler.Default);
return taskTask.Unwrap();
}
Upvotes: 1
Reputation: 74605
When I first read the title of your question I thought "Eh? Who would propose trying to assign a value of type x to variable of type y, not in an inheritance relationship with x? That's like trying to assign an int to a string..."
I read the code, and that changed to "OK, this isn't assigning a delegate to a Task, this is just creating a Task and storing it in a collection of Tasks.. But it does look like they're assigning a delegate to a Task...
Then I saw
Note my technique of invoking the lambda method immediately via the () after the the lambda block is closed {}. This ensures a Task is returned, not a Func.
The fact that you have to explain this with commentary means it's a code smell and the wrong thing to do. Your code has gone from being readably self documenting, to a code golf exercise, using an arcane syntax trick of declaring a delegate and immediately executing it to create a Task. That's what we have Task.Run
/TaskFactory.StartNew
for, and it's what all the TAP code I've seen does when it wants a Task
You'll note that this form works and doesn't produce an error:
HashSet<Task<(string Value, int ToNodeId)>> regionTasks =
new HashSet<Task<(string Value, int ToNodeId)>>();
foreach (Connection connection in Connections[RegionName])
{
regionTasks.Add(Task.Run(async () =>
{
string value = await connection.GetValueAsync(Key);
return (value, connection.ToNode.Id);
}));
}
It is far more clear how it works and the 7 characters you saved in not typing Task.Run
means you don't have to write a 50+ character comment explaining why something that looks like a delegate can be assigned to a variable of type Task
I'd say the C# compiler was saving you from writing bad code here, and it's another case of the VB compiler letting developers play fast an loose and writing hard to understand code
Upvotes: 1
Reputation: 141980
If .NET Standard 2.1
(or some .NET Framework versions, see compatibility list) is available for you, you can use LINQ with ToHashSet
method:
var regionTasks = Connections[RegionName]
.Select(async connection =>
{
string value = await connection.GetValueAsync(Key);
return (Value: value, ToNodeId: connection.ToNode.Id);
})
.ToHashSet();
Or just initialize HashSet
with corresponding IEnumerable
.
UPD
Another workaround from linked in comments answer:
static Func<R> WorkItOut<R>(Func<R> f) { return f; }
foreach (Connection connection in Connections[RegionName])
{
regionTasks.Add(WorkItOut(async () =>
{
string value = await connection.GetValueAsync(Key);
return (value, connection.ToNode.Id);
})());
}
Upvotes: 3