在 C# 和 VB.NET 中将异步 lambda 方法分配给类型为任务的变量

问题描述 投票:0回答:3

这在 C# 中可能吗?以下代码会产生编译器错误。

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);
    }());
}

C# 编译器抱怨,“错误 CS0149:预期的方法名称。” 它无法推断 lambda 方法的返回类型。

注意我在 lambda 块关闭后立即通过 () 调用 lambda 方法的技术 {}。这确保返回

Task
,而不是
Func
.

VB.NET 编译器理解这种语法。我惊讶地发现了一个 VB.NET 编译器胜过 C# 编译器的例子。有关完整故事,请参阅我的异步 Lambda 编译器错误,其中 VB 智胜 C# 博客文章。

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

VB.NET 编译器理解

End Function()
技术。它正确地推断出 lambda 方法的返回类型是
Function() As Task(Of (Value As String, ToNodeId As Integer))
,因此调用它会返回一个
Task(Of (Value As String, ToNodeId As Integer))
。这可分配给
regionTasks
变量。

C# 要求我将 lambda 方法的返回值转换为

Func
,这会产生非常难以辨认的代码。

regionTasks.Add(((Func<Task<(string Values, int ToNodeId)>>)(async () =>
{
    string value = await connection.GetValueAsync(Key);
    return (value, connection.ToNode.Id);
}))());

太可怕了。括号太多了!我在 C# 中能做的最好的事情就是显式声明一个

Func
,然后立即调用它。

Func<Task<(string Value, int ToNodeId)>> getValueAndToNodeIdAsync = async () =>
{
    string value = await connection.GetValueAsync(Key);
    return (value, connection.ToNode.Id);
};
regionTasks.Add(getValueAndToNodeIdAsync());

有没有人找到更优雅的解决方案?

c# vb.net asynchronous task anonymous-function
3个回答
3
投票

如果

.NET Standard 2.1
(或某些 .NET Framework 版本,请参阅兼容性列表)适用于您,您可以将 LINQ 与
ToHashSet
方法一起使用:

var regionTasks = Connections[RegionName]
    .Select(async connection => 
    {        
        string value = await connection.GetValueAsync(Key);
        return (Value: value, ToNodeId: connection.ToNode.Id);
    })
    .ToHashSet();

或者用相应的

HashSet
初始化
IEnumerable

更新

评论中链接的另一种解决方法答案

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);
    })());
}

1
投票

当我第一次读到你的问题的标题时,我想“嗯?谁会建议尝试将类型 x 的值分配给类型 y 的变量,而不是与 x 的继承关系?这就像试图将一个 int 分配给一个字符串……”

我阅读了代码,然后更改为“好的,这不是将委托分配给任务,这只是创建一个任务并将其存储在任务集合中......但看起来他们正在分配一个任务委托给任务...

然后我看到了

注意我在 lambda 块关闭后立即通过 () 调用 lambda 方法的技术 {}。这确保返回一个 Task,而不是一个 Func。

你必须用评论来解释这个事实意味着它是一种代码味道,是错误的做法。您的代码已经从可读的自我记录变成了代码高尔夫练习,使用神秘的语法技巧声明委托并立即执行它以创建任务。这就是我们

Task.Run
/
TaskFactory.StartNew
的目的,这也是我看到的所有 TAP 代码在需要 Task 时所做的

您会注意到此表单有效且不会产生错误:

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);
    }));
}

更清楚它是如何工作的,你在不打字时节省了 7 个字符

Task.Run
意味着你不必写 50 多个字符的注释来解释为什么看起来像委托的东西可以分配给类型的变量任务

我会说 C# 编译器使您免于在这里编写糟糕的代码,这是 VB 编译器的另一个例子,它让开发人员可以自由发挥并编写难以理解的代码


1
投票

调用异步 lambda 以获得物化任务的简单方法是使用像下面的

Run
这样的辅助函数:

public static Task Run(Func<Task> action) => action();
public static Task<TResult> Run<TResult>(Func<Task<TResult>> action) => action();

用法示例:

regionTasks.Add(Run(async () =>
{
    string value = await connection.GetValueAsync(Key);
    return (value, connection.ToNode.Id);
}));

Run
Task.Run
类似,不同之处在于
action
在当前线程上同步调用,而不是卸载到
ThreadPool
。另一个区别是
action
直接抛出的异常将被同步重新抛出,而不是被包裹在生成的
Task
中。假设
action
将是一个内联的
async
lambda,就像上面的用法示例一样,它无论如何都会进行这种包装,因此添加另一个包装器会过大。如果你想消除这种差异,并使其与
Task.Run
更相似,你可以使用
Task
构造函数,以及
RunSynchronously
Unwrap
方法,如下所示:

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();
}
© www.soinside.com 2019 - 2024. All rights reserved.