【问题标题】:Run enumeration of IAsyncEnumerable twice not possible?不可能两次运行 IAsyncEnumerable 的枚举?
【发布时间】:2020-06-28 16:59:15
【问题描述】:

不可能两次枚举IAsyncEnumerable

CountAsync 运行后,await foreach 将不会枚举任何项目。为什么? AsyncEnumerator上好像没有Reset方法。

var count = await itemsToImport.CountAsync();

await foreach (var importEntity in itemsToImport)
{
    // won't run
}

数据来源:

private IAsyncEnumerable<TEntity> InternalImportFromStream(TextReader reader)
{
    var csvReader = new CsvReader(reader, Config);
        
    return csvReader.GetRecordsAsync<TEntity>();
}

【问题讨论】:

  • 即使是同步枚举器也几乎从未真正实现.Reset(); IAsyncEnumerator 只是编纂了您不能多次列举的做法。至于IAsyncEnumerable,同样适用于IEnumerable:是否可以多次枚举在接口中没有定义,但对于许多来源你不能,因为会有隐藏的性能损失或不一致的结果(就像执行两次数据库查询一样)。您必须明确处理,这意味着您必须实现结果(.ToList() 和相关)或自己重做操作。
  • 如果您不需要开始计数,那么您可以在将计数递增 1 的同时迭代每个项目,因此您不必将所有内容都放入内存,同时仍然获得计数一次'已经处理了集合。
  • 对于某些来源,您无法在枚举“之前”获得计数(例如,从数据库中流式传输行)。 .Reset(),即使它存在,也要求将结果缓冲在某处,否则一切都将重做。如果你想自己缓冲/重做它们,你可以,但你不能指望源在你想等待计数的情况下为你做这件事。集合实现了自己的 .Count 属性,您可以使用这些属性“直接”获取计数;类似的机制可用于其他来源,具体取决于它们的性质(例如在 Web API 上单独调用 Count)。
  • 如果数据实际上已经加载(例如,驻留在内存中),则异步枚举它无论如何都不会添加任何内容,您不妨使用同步枚举(大概可以重做)。另一方面,如果它不是常驻的并且使用异步枚举来封装异步(文件)I/O,这会更常见,你必须有意识地选择两次读取文件,并且接口强制你明确地这样做。
  • 正如大卫布朗所说,这不是因为IAsyncEnumerable,而是因为实现它的底层类型。我建议阅读这篇文章。有一个例子不管它被迭代多少次都有效:docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/…

标签: c# async-await c#-8.0 iasyncenumerable


【解决方案1】:

这与重置 IAsyncEnumerator 无关。此代码尝试生成第二个 IAsyncEnumerator,就像 IEnumerable.GetEnumerator() 一样,它只能在某些类型的集合上使用。如果 Enumerable(异步与否)是对某种只进数据结构的抽象,则 GetEnumerator/GetAsyncEnumerator 将失败。

即使没有失败,有时也很昂贵。例如,它可能会在每次枚举时运行数据库查询或访问远程 API。这就是为什么 IEnumerable/IAsyncEnumerable 使函数的公共返回类型很差的原因,因为它们无法描述返回集合的功能,并且几乎唯一可以对值做的事情就是使用 .ToList/ToListAsync 实现它。

例如,这很好用:

static async IAsyncEnumerable<int> Col()
{
    for (int i = 1; i <= 10; i++)
    {
        yield return i;
    }
}
static void Main(string[] args)
{
    Run().Wait();
}
static async Task Run()
{

    var col = Col();

    var count = await col.CountAsync();
    await foreach (var dataPoint in col)
    {
        Console.WriteLine(dataPoint);
    }
}

【讨论】:

    【解决方案2】:

    似乎无法通过接口本身重置 IAsyncEnumerable,因为 IAsyncEnumerator 接口上没有 Reset 方法。

    在此特定示例中,第二个枚举将不起作用,因为 IAsyncEnumerable 以 Stream 为目标。读取流后,位置光标将指向流的末端。如果您可以控制流或对它的引用(我没有),您可以再次将位置设置为 0 并再次枚举它。

    我倾向于使用 ToListAsync,然后从其 Count 属性中获取计数并同步迭代项目,因为它们已经加载。

    【讨论】:

    • IAsyncEnumerator 旨在成为一次性一次性物品。这并不意味着IAsyncEnumerable 只能枚举一次,因为可枚举是可以创建无限数量的枚举器的工厂。每次调用 GetAsyncEnumerator 方法时,都会得到一个全新的枚举器。但不能保证所有这些枚举器都会产生相同的序列。
    • 正如我所说:对于流,它不起作用,因为流的位置在其末尾,需要再次设置为 0,然后再尝试再次对其进行迭代。
    • Sven 这取决于实现。可以轻松实现IAsyncEnumerable&lt;T&gt;,每次枚举它时都会创建一个新的CsvReader,并从位置0 开始重新调用它。为此,您只需将async 修饰符添加到InternalImportFromStream,然后循环和yield 包含在GetRecordsAsync 方法中的元素。
    猜你喜欢
    • 2023-03-26
    • 2019-10-24
    • 1970-01-01
    • 1970-01-01
    • 2020-11-17
    • 2021-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多