【问题标题】:Assign C# Async Lambda Method to Variable Typed as a Task将 C# 异步 Lambda 方法分配给类型为任务的变量
【发布时间】:2020-10-25 06:58:08
【问题描述】:

这在 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# 编译器更聪明。请参阅我的 An Async Lambda Compiler Error Where VB Outsmarts 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());

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

【问题讨论】:

  • IIFE 风格的问题在于编译器还没有确定它是表达式树还是匿名方法。有时很烦人,但很少遇到。
  • 这阐明了为什么 C# 编译器无法推断返回类型。虽然我仍然很困惑为什么 VB.NET 编译器可以。我想我需要阅读 .NET 表达式树。
  • 确实很奇怪。我希望 VB 假定它是一个匿名方法。如果是这样,一个实际的选择。
  • @ErikMadsen 似乎表达树are not related 的问题。
  • @GuruStron 我不这么认为。我声明了 regionTasks 变量的类型。提问者表示他的代码的强类型版本确实可以编译。他只遇到了 var 类型版本的编译器错误。

标签: c# vb.net asynchronous task anonymous-function


【解决方案1】:

如果您可以使用.NET Standard 2.1(或某些 .NET Framework 版本,请参阅compatibility list),您可以将 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

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

【讨论】:

  • 真是太聪明了!谢谢。
  • 为什么我知道我的问题会得到 Eric Lippert 的回答?哈哈。我是他博客的狂热读者,完全尊重他为向我们所有人介绍 .NET 运行时和 C# 编译器的内部工作原理所做的努力。尽管在这种特殊情况下(您链接到的问题),我同意 user541686 对 Eric 的回答的评论,“将其说明为不可能的事情有点误导,因为这实际上在 D 中完全可以正常工作。只是你们没有选择给委托文字自己的类型,而是让它们依赖于它们的上下文......"
  • 感谢您的第二次推荐。这也是 Theodor Zoulias 所建议的,虽然函数名称更实用,Materialize。 WorkItOut(我知道这是 Lippert 的术语,不是你的)让我觉得非常具有教育意义。
【解决方案2】:

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

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

然后我看到了

请注意我在 lambda 块关闭 {} 后立即通过 () 调用 lambda 方法的技术。这样可以确保返回的是 Task,而不是 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);
    }));
}

它的工作原理要清楚得多,您在不输入 Task.Run 时节省的 7 个字符意味着您不必编写 50 多个字符的注释来解释为什么可以将看起来像委托的东西分配给变量任务类型

我会说 C# 编译器可以让你避免在这里编写糟糕的代码,这是 VB 编译器让开发人员快速松散并编写难以理解的代码的另一种情况

【讨论】:

  • 赞成。应该注意的是,OP 可能有正当理由避免使用Task.Run。例如,它们可以在 lambda 中操作 UI 元素,因此可能需要保持在当前同步上下文中。
  • Task.Run 包装异步操作会浪费资源。基于名称connection.GetValueAsync 方法访问外部资源。所以我会说代码Task.Run(async () =&gt; ...) 是代码气味
  • Task.Run 用于 CPU 密集型工作。我的代码非常受 I/O 限制——等待 HTTP 响应。 docs.microsoft.com/en-us/dotnet/csharp/programming-guide/….
  • @CaiusJard。我明白你在说什么。你是正确的,我明确声明一个 Func,然后在下一行调用它的技术是有效的。尽管我认为您在“您会注意到此表单有效......”评论中包含了错误的代码。我认为您将我的初始尝试代码称为高尔夫太苛刻了。我相信它本着匿名函数的精神,旨在减少代码仪式。我只是想了解为什么 C# 编译器不理解我的代码。我觉得 VB.NET 编译器具有讽刺意味,考虑到 VB 往往会强制执行过于冗长的代码。
  • 谢谢@TheodorZoulias。 Stephen Toub 绝对是专家。
【解决方案3】:

调用异步 lambda 以获取具体化任务的一种简单方法是使用一个辅助函数,如下面的 Materialize

public static Task Materialize(Func<Task> taskFactory) => taskFactory();
public static Task<T> Materialize<T>(Func<Task<T>> taskFactory) => taskFactory();

使用示例:

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

【讨论】:

  • 这很干净。我喜欢。我很好奇,您编写这些辅助方法是因为您在自己的代码中遇到了这个问题吗?还是说根本原因很清楚(此时包含所有示例代码和注释)并且您想通过辅助方法简单地克服编译器的缺点?
  • @ErikMadsen 后者。 :-) 实际上,我在很多情况下都需要具体化 lambda,但我从未想过要使用像上面这样的通用 Materialize 方法。我总是选择使用本地函数,或者Task.Run,或者在手头的情况下可以做的任何事情。
  • 是的,也许我应该摆脱对本地函数的困扰,或者明确声明 Func 然后立即调用它——并接受它们作为合理的技术。这归结为开发人员的首选风格,真的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-05
  • 2012-03-03
  • 1970-01-01
  • 2013-11-06
  • 2018-08-16
相关资源
最近更新 更多