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

Pau*_*rth 12 .net c# linq

有很多Linq算法只需要通过输入一次,例如Select.

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

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

这意味着您无法在任何正在读取"已打开"的流的情况下使用Linq.

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

简而言之,我有一个"已经打开"的结果流,我可以将其转换为适当的一次性IEnumerator - 但不幸的是,所有下游代码都需要IEnumerable而不是IEnumerator,即使它只会执行一次"传递".

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

class TabularStream 
{ 
    Column[] Columns; 
    IEnumerator<object[]> RowStream; 
}
Run Code Online (Sandbox Code Playgroud)

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

我所知道的最好的解决方法是实现一个IEnumerable,其GetEnumerator()方法返回唯一的IEnumerator,如果某些东西试图进行两次GetEnumerator()调用,则会抛出错误.

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

Jon*_*eet 14

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

首先,它编码的事实是它具有破坏性 - 而LINQ查询通常可以多次运行.它们意味着没有副作用,而迭代a的行为IEnumerator<T>自然会产生副作用.

它还使得在LINQ to Objects中执行某些优化几乎是不可能的,例如,Count如果您实际上要求ICollection<T>其计数,则使用该属性.

至于你的解决方法:是的,OneShotEnumerable一个合理的方法.


sta*_*ica 8

虽然我普遍同意Jon Skeet的回答,但我也遇到过很少的情况,在这种情况下工作IEnumerator确实比将它们包装成一次只更合适IEnumerable.

我将首先阐述一个这样的案例并描述我自己的问题解决方案.

案例:仅向前,不可重绕的数据库游标

ESRI用于访问地理数据库的API(ArcObjects)具有无法重置的仅向前数据库游标.它们本质上就是API的等价物IEnumerator.但没有相当于IEnumerable.因此,如果您想以".NET方式"包装该API,您有三个选项(我按以下顺序进行了探讨):

  1. 将光标包裹为IEnumerator(因为它就是它的真实情况)并直接使用它(这很麻烦).

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

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

最后一个选项是我通常会针对类似情况推荐的实用解决方案(如果适用).如果您正在寻找其他解决方案,请继续阅读.


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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 操作员必须永远不会返回IEnumerable<T>,因为这意味着你可以打破你自己的"LINQ to IEnumerator"世界并逃到常规的LINQ.在那里,你最终会遇到上面已经描述的不可重复性问题.

  • 您无法使用foreach循环处理某些查询的结果...除非IEnumerator<T>查询运算符返回的每个对象都实现了GetEnumerator返回的方法this.提供额外的方法意味着您不能使用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需要相当长的时间......问题是,它真的值得付出努力吗?