【问题标题】:Why don't the Linq extension methods sit on IEnumerator rather than IEnumerable?为什么 Linq 扩展方法不在 IEnumerator 而不是 IEnumerable 上?
【发布时间】:2011-04-13 02:54:40
【问题描述】:

有很多 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; 
}

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

我所知道的最好的解决方法是实现一个 IEnumerable,它的 GetEnumerator() 方法返回唯一的 IEnumerator,如果尝试调用 GetEnumerator() 两次,则会引发错误。

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

【问题讨论】:

    标签: c# .net linq


    【解决方案1】:

    在我看来,直接使用IEnumerator&lt;T&gt; 并不是一个好主意。

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

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

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

    【讨论】:

      【解决方案2】:

      虽然我大体上同意Jon Skeet's answer,但我也遇到过极少数情况,使用IEnumerator 确实比将它们包装在一次-IEnumerable 中更合适.

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

      案例示例:只进、不可回退的数据库游标

      ESRI 用于访问地理数据库的 API (ArcObjects) 具有无法重置的只进数据库游标。它们本质上是 API 的 IEnumerator 等价物。但是没有等同于IEnumerable。因此,如果您想以“.NET 方式”封装该 API,您有三个选项(我按以下顺序探索):

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

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

      3. 不要将 光标本身包装为 IEnumerable,而是将可用于检索 a 光标(例如查询条件和对正在查询的数据库对象的引用)。这样,只需重新执行整个查询,就可以进行多次迭代。这是我当时最终决定的。

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


      IEnumerator&lt;T&gt; 接口重新实现LINQ 查询运算符?

      在技术上可以为IEnumerator&lt;T&gt; 接口实现部分或全部 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&lt;T&gt;,因为这意味着您可以突破自己的“LINQ to IEnumerator”世界并转入常规LINQ。您最终会遇到上面已经描述的不可重复性问题。

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

        这很奇怪,可能是对IEnumerator&lt;T&gt;foreach 构造的滥用。

      • 如果禁止返回IEnumerable&lt;T&gt; 并且返回IEnumerator&lt;T&gt; 很麻烦(因为foreach 不起作用),为什么不返回普通数组呢?因为这样查询就不能再懒惰了。


      IQueryable + IEnumerator = IQueryator

      将查询的执行延迟到完全组合后会怎样?在IEnumerable 的世界里,这就是IQueryable 所做的;所以理论上我们可以构建一个IEnumerator 等价物,我称之为IQueryator

      • IQueryator 可以检查逻辑错误,例如在序列完全被Count 等前面的操作消耗后对序列执行任何操作。 IE。像Count 这样的所有消耗运算符总是必须是查询运算符连接中的最后一个。

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

      实施IQueryator 需要相当长的时间...问题是,真的值得付出努力吗?

      【讨论】:

      • 另一个用例是当您需要确定性完成时。如果您返回一个挂在某个 IDisposable 上的 IEnumerable,您将无法控制处置。使用 IEnumerator,调用方法可以在不需要对象时立即处理它。
      猜你喜欢
      • 2011-08-07
      • 2011-11-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多