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

时间:2010-09-16 09:17:10

标签: c# .net linq

有许多Linq算法只需要通过输入进行一次传递,例如选择。

然而,所有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; 
}

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

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

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

2 个答案:

答案 0 :(得分:14)

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

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

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

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

答案 1 :(得分:7)

虽然我通常同意Jon Skeet's answer,但我也遇到过很少的情况,使用IEnumerator确实比将它们包装在一次只更合适 - IEnumerable

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

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

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

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

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

    < / LI>
  3. 不要将 游标本身包装为IEnumerable,而是可以用来检索 a 游标(例如,查询条件和对要查询的数据库对象的引用)。这样,几次迭代就可以简单地重新执行整个查询。这是我当时最终决定的。

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


    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>对象都实现了返回{GetEnumerator的{​​{1}}方法1}}。提供额外的方法意味着您不能使用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需要相当长的时间......问题是,它真的值得努力吗?