将 C# 异步 Lambda 方法分配给作为任务键入的变量

Assign C# Async Lambda Method to Variable Typed as a Task

这在 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 方法的 return 类型。

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

VB.NET 编译器理解这种语法。我惊讶地发现 VB.NET 编译器胜过 C# 编译器的示例。有关完整故事,请参阅我的 An Async Lambda Compiler Error Where VB Outsmarts C# 博客 post。

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 方法的 return 类型是 Function() As Task(Of (Value As String, ToNodeId As Integer)),因此调用它 return 是 Task(Of (Value As String, ToNodeId As Integer))。这可分配给 regionTasks 变量。

C# 要求我将 lambda 方法的 return 值转换为 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 版本,请参见compatibility list) is available for you, you can use LINQ with 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

评论中链接的另一种解决方法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);
    })());
}

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

我阅读了代码,它变成了“好的,这不是将委托分配给任务,这只是创建一个任务并将其存储在 collection 个任务中..但是它看起来他们正在将委托分配给任务...

然后我看到了

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.

你必须用评论来解释这一事实意味着这是一种代码味道,是错误的做法。您的代码已经从可读的自我记录变成了代码高尔夫练习,使用神秘的语法技巧声明委托并立即执行它以创建任务。这就是我们 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 编译器的另一个例子,让开发人员可以自由发挥并编写难以理解的代码

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