10

我有以下界面:

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方法时,您可以 return anIEnumerable或 yield return 多个值。为什么我不允许这样做IAsyncEnumerable

4

2 回答 2

7

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

该规范声明yield结果的存在存在于迭代器方法中;以及两者的存在async导致yield异步迭代器方法。

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

关键字是把async它变成一个异步迭代器方法。既然你需要asyncfor the await,那么你也需要使用yield

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

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

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

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

  • 常规方法。没有asyncyield存在。
  • 迭代器方法。Ayield在体内没有async。必须返回IEnumerable<T>(或IEnumerator<T>)。
  • 异步方法。Anasync在没有 的情况下存在yield。必须返回一个类似任务。
  • 异步迭代器方法。两者asyncyield存在。必须返回IAsyncEnumerable<T>(或IAsyncEnumerator<T>)。

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

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

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

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

附带说明一下,在这些情况下,最好同时拥有yield*同步和异步迭代器,但 C# 不支持。

于 2021-01-27T14:23:18.960 回答
2

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

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

如果你做了它async但没有返回 aTask<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

于 2021-01-27T10:59:00.220 回答