有许多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”?
答案 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,您有三个选项(我按以下顺序进行了探讨):
将光标包裹为IEnumerator
(因为它实际上是这样)并直接使用它(这很麻烦)。
将光标或来自(1)的包裹IEnumerator
包裹为一次IEnumerable
(使其与LINQ兼容且通常更易于使用)。这里的错误是不是 IEnumerable
,因为它不能被多次枚举,这可能会被代码的用户或维护者忽略。
不要将 游标本身包装为IEnumerable
,而是可以用来检索 a 游标(例如,查询条件和对要查询的数据库对象的引用)。这样,几次迭代就可以简单地重新执行整个查询。这是我当时最终决定的。
最后一个选项是我通常会针对类似情况推荐的实用解决方案(如果适用)。如果您正在寻找其他解决方案,请继续阅读。
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
需要相当长的时间......问题是,它真的值得努力吗?