为啥我不允许在返回 IAsyncEnumerable 的方法中返回 IAsyncEnumerable

Posted

技术标签:

【中文标题】为啥我不允许在返回 IAsyncEnumerable 的方法中返回 IAsyncEnumerable【英文标题】:Why am I not allowed to return an IAsyncEnumerable in a method returning an IAsyncEnumerable为什么我不允许在返回 IAsyncEnumerable 的方法中返回 IAsyncEnumerable 【发布时间】:2021-05-01 04:18:45 【问题描述】:

我有如下界面:

public interface IValidationSystem<T>

    IAsyncEnumerable<ValidationResult> ValidateAsync(T obj);

我正在尝试以这种方式实现它:

public class Foo
 

public class Bar
 

public class BarValidationSystem : IValidationSystem<T>
   
    public async IAsyncEnumerable<ValidationResult> ValidateAsync(Bar bar)
    
        var foo = await GetRequiredThingAsync();

        return GetErrors(bar, foo).Select(e => new ValidationResult(e)).ToAsyncEnumerable();
    

    private static IEnumerable<string> GetErrors(Bar bar, Foo foo)
    
        yield return "Something is wrong";
        yield return "Oops something else is wrong";
        yield return "And eventually, this thing is wrong too";
    
    
    private Task<Foo> GetRequiredThingAsync()
    
        return Task.FromResult(new Foo());
    

但这不能编译:

CS1622 无法从迭代器返回值。使用 yield return 语句返回一个值,或者使用 yield break 结束迭代。

我知道我可以通过迭代枚举来修复:

foreach (var error in GetErrors(bar, foo))

    yield return new ValidationResult(error);

或者通过返回 Task&lt;IEnumerable&lt;ValidationResult&gt;&gt;:

public async Task<IEnumerable<ValidationResult>> ValidateAsync(Bar bar)

    var foo = await GetRequiredThingAsync;

    return GetErrors(bar, foo).Select(e => new ValidationResult(e));

但我想了解为什么我不能返回 IAsyncEnumerable。在编写“经典”IEnumerable 方法时,您可以返回 IEnumerable 或 yield 返回多个值。为什么我不能对 IAsyncEnumerable 做同样的事情?

【问题讨论】:

我试图查看这个,但是缺少太多东西来编译这个。你能提供一个minimal reproducible example(最好有小提琴链接)吗? @fharreau:不,fiddle 仍然错过了ToAsyncEnumerable() 的定义。抱歉,我出去了……编译的工作量太大(需要重新开始工作)。 相关:Pass-through for IAsyncEnumerable? 在阅读spec proposal 时,这看起来像是一个错误,或者至少是一个无意的限制。其意图显然是通过使用yield 来指示异步迭代器,就像同步迭代器一样;然而,async 和返回类型的组合似乎将其锁定为迭代器。 @Jeroen:这就是我对情况的理解。谢谢你把它写成一个清晰的句子 【参考方案1】:

在阅读spec proposal 时,这看起来像是一个错误或至少是一个无意的限制。

规范声明yield 的存在导致迭代器方法; asyncyield 的存在导致异步迭代器方法。

但我想了解为什么我不能返回 IAsyncEnumerable。

async 关键字使它成为一个异步迭代器方法。由于await 需要async,因此您还需要使用yield

在编写“经典”IEnumerable 方法时,您可以返回一个 IEnumerable 或 yield 返回多个值。为什么我不能对 IAsyncEnumerable 做同样的事情?

同时使用IEnumerable&lt;T&gt;IAsyncEnumerable&lt;T&gt;,您可以在直接返回可枚举之前执行同步工作。在这种情况下,方法一点也不特别;它只是做一些工作,然后返回一个值给它的调用者。

但是你不能在返回一个异步枚举器之前做异步工作。在这种情况下,您需要 async 关键字。添加async 关键字会强制该方法为异步方法或异步迭代器方法。

换句话说,所有方法在C#中都可以分为这些不同的类型:

普通方法。没有asyncyield 存在。 迭代器方法。没有async 的正文中的yield。必须返回IEnumerable&lt;T&gt;(或IEnumerator&lt;T&gt;)。 异步方法。一个async 存在,但没有yield。必须返回一个 tasklike。 异步迭代器方法。 asyncyield 都存在。必须返回IAsyncEnumerable&lt;T&gt;(或IAsyncEnumerator&lt;T&gt;)。

从另一个角度,考虑实现这种方法所必须使用的状态机,尤其要考虑await GetRequiredThingAsync() 代码何时运行。

在同步世界中没有yieldGetRequiredThing() 将运行之前返回可枚举。在 yield 的同步世界中,GetRequiredThing() 将在请求可枚举的第一项时运行。

在异步世界中没有yieldawait GetRequiredThingAsync() 将在返回异步枚举之前运行(在这种情况下,返回类型将为Task&lt;IAsyncEnumerable&lt;T&gt;&gt; ,因为您必须进行异步工作才能获取异步枚举)。在 yield 的异步世界中,await GetRequiredThingAsync() 将在请求可枚举的第一项时运行。

一般来说,您想要在返回可枚举之前完成工作的唯一情况是您进行前置条件检查(本质上是同步的)。进行 API/DB 调用是不正常的;大多数时候,预期的语义是任何 API/DB 调用都将作为 枚举 的一部分完成。换句话说,即使是同步代码也可能应该使用foreachyield,就像异步代码是强制这样做的。

附带说明一下,在这些场景中,同步和异步迭代器都有一个 yield* 会很好,但 C# 不支持。

【讨论】:

【参考方案2】:

异步方法的常用语法是返回一个任务:

public async Task<Foo> GetFooAsync()

    /...

如果您将其设为async 但未返回Task&lt;T&gt;,则编译器将在标头中标记错误。

有一个例外:一个迭代器方法返回IAsyncEnumerable

private static async IAsyncEnumerable<int> ThisShouldReturnAsTask()

    yield return 0;
    await Task.Delay(100);
    yield return 1;

在您的示例中,您有一个返回 IAsyncEnumerable 的异步函数,但它不是迭代器。 (它只是返回一个直接的可枚举对象,而不是一个接一个地产生值。)

因此出现错误:“无法从迭代器返回值”

如果您将签名更改为返回Task&lt;IAsyncEnumerable&lt;ValidationResult&gt;&gt;

public async Task<IAsyncEnumerable<ValidationResult>> ValidateAsync(Bar bar)

    var foo = await GetRequiredThingAsync();
    return GetErrors(bar, foo).Select(e => new ValidationResult(e)).ToAsyncEnumerable();

那么你需要改变你调用它的方式:它必须是

await foreach(var f in await ValidateAsync(new Bar()))

代替

await foreach(var f in ValidateAsync(new Bar()))

在此处查看示例:https://dotnetfiddle.net/yPTdqp

【讨论】:

不,绝对不是。可以返回异步 IAsyncEnumerable,检查新的 C# 8 功能:docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/… @fharreau:好的,我不知道这一点。但在这些示例中,它是一个迭代器方法。我的猜测是,如果它没有返回一个 Task 那么它是一个迭代器方法。我会修改我的答案。 返回Task&lt;IAsyncEnumerable&lt;...&gt;&gt; 的方法与返回IAsyncEnumerable 的方法不同,即使后一种方法是async。返回Task&lt;IAsyncEnumerable&lt;...&gt;&gt; 的方法不能在await foreach 中使用;该方法的结果本身需要等待。 这里真正的问题是在方法签名中使用async关键字将方法变成iterator method(可能不是正确的措辞),强制使用@987654340 @(在链接的问题中查看 Stephen Cleary 的 2 个答案) @fharreau 感谢您发布问题。我的回答产生了有趣的 cmets,所以我将其留在这里而不是删除它。

以上是关于为啥我不允许在返回 IAsyncEnumerable 的方法中返回 IAsyncEnumerable的主要内容,如果未能解决你的问题,请参考以下文章

为啥允许在 Python 中使用 super 从 __init__ 返回一个值?

为啥 MySQL 给出错误“不允许从函数返回结果集”?

如果我不首先抛出 FileNotFoundExcetion,为啥 file.exists() 总是返回 true

为啥 Rust 的示例猜谜游戏允许具有不同返回类型的 match 语句?

为啥 C++ lower_bound() 允许返回与 val 等效的指针,而 upper_bound() 不允许

为啥即使我不滚动,dispatch_semaphore_wait() 也一直返回 YES?