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

问题描述

我有以下界面:

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<IEnumerable<ValidationResult>>

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 做同样的事情?

解决方法

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

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

但我想了解为什么在我的情况下我不能返回 IAsyncEnumerable。

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

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

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

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

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

  • 常规方法。不存在 asyncyield
  • 迭代器方法。正文中没有 yieldasync。必须返回 IEnumerable<T>(或 IEnumerator<T>)。
  • 异步方法。存在 async 而没有 yield。必须返回一个 tasklike。
  • 异步迭代器方法。 asyncyield 都存在。必须返回 IAsyncEnumerable<T>(或 IAsyncEnumerator<T>)。

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

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

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

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

顺便提一下,在这些场景中,同步和异步迭代器都使用 yield* 会很好,但 C# 不支持。

,

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

public async Task<Foo> GetFooAsync()
{
    /...
}

如果你让它async但没有返回Task<T>,那么编译器会在标头中标记一个错误。

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

private static async IAsyncEnumerable<int> ThisShouldReturnAsTask()
{
    yield return 0;
    await Task.Delay(100);
    yield return 1;
}

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

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

如果您将签名更改为返回 Task<IAsyncEnumerable<ValidationResult>>

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...