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



这在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());

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

如果.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();

或者只是用相应的IEnumerable初始化HashSet.

UPD

注释中链接的另一种解决方法回答:

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

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

我阅读了代码,并更改为"好吧,这不是为任务分配委托,这只是创建一个任务并将其存储在任务集合中。但看起来他们确实在为任务分配委托......

然后我看到了

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

你必须用评论来解释这一点,这意味着这是一种代码气味和错误的做法。您的代码已经从可读的自我文档变成了代码高尔夫练习,使用一种晦涩难懂的语法技巧来声明委托并立即执行它以创建任务。这就是我们Task.Run/TaskFactory.StartNew的目的,也是我看到的所有TAP代码在需要任务时所做的

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

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 编译器让开发人员快速松散并编写难以理解的代码的另一个案例

调用异步 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);
}));

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

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

最新更新