繁体   English   中英

为什么Linq扩展方法不位于IEnumerator而不是IEnumerable?

[英]Why don't the Linq extension methods sit on IEnumerator rather than IEnumerable?

许多Linq算法只需要对输入进行一次遍历,例如Select。

但是,所有Linq扩展方法都位于IEnumerable而非IEnumerator上

    var e = new[] { 1, 2, 3, 4, 5 }.GetEnumerator(); 
    e.Select(x => x * x); // Doesn't work 

这意味着在从“已打开”流中读取的任何情况下都不能使用Linq。

对于我当前正在处理的项目,这种情况经常发生-我想返回一个IEnumerator,其IDispose方法将关闭流,并使所有下游Linq代码对此进行操作。

简而言之,我有一个“已经开放”的结果流,可以将其转换为适当的一次性IEnumerator-但不幸的是,所有下游代码都需要IEnumerable而不是IEnumerator,即使它只是要做一个“通过”。

即我想在各种不同的来源(CSV文件,IDataReaders等)上“实现”此返回类型:

class TabularStream 
{ 
    Column[] Columns; 
    IEnumerator<object[]> RowStream; 
}

为了获得“列”,我必须已经打开CSV文件,启动SQL查询或其他操作。 然后,我可以返回一个“ IEnumerator”,其Dispose方法关闭资源-但是所有Linq操作都需要一个IEnumerable。

我知道最好的解决方法是实现一个IEnumerable,它的GetEnumerator()方法返回一个和唯一的IEnumerator,并且如果某事尝试两次执行GetEnumerator()调用则抛出错误。

这一切听起来还不错,还是有一种更好的方法可以用Linq易于使用的方式实现“ TabularStream”?

在我看来,直接使用IEnumerator<T>很少是一个好主意。

一方面,它编码了具有破坏性的事实-而LINQ查询通常可以运行多次。 它们本来是没有副作用的,但是在IEnumerator<T>进行迭代的行为自然是副作用。

这实际上使在LINQ to Objects中执行某些优化几乎是不可能的,例如,如果您实际上是在向ICollection<T>询问其计数,则使用Count属性。

至于您的解决方法:是的, OneShotEnumerable是一种合理的方法。

尽管我通常都同意Jon Skeet的回答 ,但我也遇到了很少的情况,与IEnumerator一起工作确实比将它们包装在仅一次的IEnumerable更合适。

我将首先说明一个这样的案例,并描述我自己的解决方案。

案例示例:仅转发,不可撤消的数据库游标

ESRI的用于访问地理数据库( ArcObjects )的API具有只能重置的仅向前数据库游标。 从本质上讲,它们等效于IEnumerator API。 但是没有等效于IEnumerable 因此,如果您想以“ .NET方式”包装该API,则有三个选项(我按以下顺序进行了探讨):

  1. 将游标包装为IEnumerator (因为它实际上就是它)并直接使用IEnumerator (这很麻烦)。

  2. 将光标或包装为(1)的IEnumerator包装为仅一次的IEnumerable (使其与LINQ兼容,并且通常更易于使用)。 这里的错误是它不是 IEnumerable ,因为它不能被多次枚举,并且代码的用户或维护者可能会忽略它。

  3. 光标本身作为IEnumerable ,但其可用于检索光标 (例如查询条件和参照数据库对象被查询)。 这样,只需简单地重新执行整个查询,就可以进行多次迭代。 这是我当时最终决定的。

最后一个选择是实用的解决方案,对于类似情况(如果适用),我通常会建议这样做。 如果您正在寻找其他解决方案,请继续阅读。


重新实现IEnumerator<T>接口的LINQ查询运算符?

从技术上讲,可以为IEnumerator<T>接口实现LINQ的部分或全部查询运算符。 一种方法是编写一堆扩展方法,例如:

public static IEnumerator<T> Where(this IEnumerator<T> xs, Func<T, bool> predicate)
{
    while (xs.MoveNext())
    {
        T x = xs.Current;
        if (predicate(x)) yield return x;
    }
    yield break;
}

让我们考虑一些关键问题:

  • 运算符绝不能返回IEnumerable<T> ,因为这意味着您可以突破自己的“ LINQ to IEnumerator ”世界,转而进入常规的LINQ。 到此为止,您将遇到上面已经描述的不可重复性问题。

  • 您无法使用foreach循环来处理某些查询的结果…除非查询运算符返回的每个IEnumerator<T>对象都实现了返回thisGetEnumerator方法。 提供该其他方法将意味着您不能使用yield return/break ,而必须手动编写IEnumerator<T>类。

    这只是很奇怪,并且可能滥用了IEnumerator<T>foreach构造。

  • 如果返回IEnumerable<T>是被禁止的并返回IEnumerator<T>是麻烦的(因为foreach不工作),为什么不返回纯数组? 因为这样查询不再是懒惰的。


IQueryable + IEnumerator = IQueryator

将查询的执行推迟到完全组成该怎么办? IEnumerable世界中, IQueryable就是这样做的。 因此,从理论上讲,我们可以构建一个IEnumerator等效项,我将其称为IQueryator

  • IQueryator可以检查逻辑错误,例如在诸如Count类的先前操作完全消耗完序列后,对该序列执行任何操作。 也就是说,像Count这样的所有消耗大量运算符都必须始终是查询运算符串联中的最后一个。

  • IQueryator可以返回一个数组(如上面的建议)或其他只读集合,但不能由单个运算符返回; 仅在查询执行时。

实施IQueryator需要花费一些时间...问题是,实际上值得付出努力吗?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM