如何判断IEnumerable 是否会延迟执行?

我总是假设如果我在LINQ对象的上下文中使用Select(x=> ...) ,那么新的集合将立即创建并保持静态。 我不太清楚为什么我这么做,这是一个非常糟糕的假设,但我做到了。 我经常在其他地方使用.ToList() ,但在这种情况下通常不会。

此代码演示即使是简单的“选择”也会延迟执行:

 var random = new Random(); var animals = new[] { "cat", "dog", "mouse" }; var randomNumberOfAnimals = animals.Select(x => Math.Floor(random.NextDouble() * 100) + " " + x + "s"); foreach (var i in randomNumberOfAnimals) { testContextInstance.WriteLine("There are " + i); } foreach (var i in randomNumberOfAnimals) { testContextInstance.WriteLine("And now, there are " + i); } 

这将输出以下内容(每次迭代集合时都会调用随机函数):

 There are 75 cats There are 28 dogs There are 62 mouses And now, there are 78 cats And now, there are 69 dogs And now, there are 43 mouses 

我有很多地方,我有一个IEnumerable作为一个类的成员。 通常,LINQ查询的结果被分配给这样的IEnumerable 。 通常对我来说这不会引起问题,但我最近在我的代码中找到了一些不仅仅是性能问题的地方。

在尝试检查我犯了这个错误的地方时,我想我可以检查一下特定的IEnumerable是否属于IQueryable类型。 我想这会告诉我collections是否“延期”。 事实certificate,上面的Select运算符创建的枚举器的类型为System.Linq.Enumerable+WhereSelectArrayIterator``[System.String,System.String]而不是IQueryable

我使用Reflector来查看这个接口inheritance了什么,并且结果表明它不会inheritance任何表明它是’LINQ’的东西 – 所以没有办法根据集合类型进行测试。

我现在非常高兴地把.ToArray()现在放在各处,但是我希望有一个机制来确保将来不会发生这个问题。 Visual Studio似乎知道如何做到这一点,因为它提供了一条关于“扩展结果视图将评估集合”的消息。

我想出的最好的是:

 bool deferred = !object.ReferenceEquals(randomNumberOfAnimals.First(), randomNumberOfAnimals.First()); 

编辑:这仅在使用“选择”创建新对象且不是通用解决方案时才有效。 不管怎样我都不推荐它! 这是一个解决方案的一点点舌头。

延迟执行LINQ困扰了很多人,你并不孤单。

我为避免这个问题采取的方法如下:

方法的参数 – 使用IEnumerable除非需要更具体的接口。

局部变量 – 通常在我创建LINQ的时候,所以我会知道是否可以进行惰性求值。

类成员 – 永远不要使用IEnumerable ,总是使用List 。 并始终将它们设为私有。

属性 – 使用IEnumerable ,并在setter中转换为存储。

 public IEnumerable People { get { return people; } set { people = value.ToList(); } } private List people; 

虽然有理论上这种方法不起作用,但我还没有遇到过这种情况,而且自从Beta版以来我一直热衷于使用LINQ扩展方法。

顺便说一句:我很好奇你为什么要使用ToArray(); 而不是ToList(); – 对我来说,列表有一个更好的API,并且(几乎)没有性能成本。

更新 :一些评论者正确地指出arrays具有理论上的性能优势,因此我将上面的陈述修改为“……几乎没有性能成本。”

更新2 :我写了一些代码来对数组和列表之间的性能差异进行一些微基准测试。 在我的笔记本电脑上,在我的特定基准测试中,每次访问的差异大约为5ns(即纳秒 )。 我想有些情况下每个循环节省5ns是值得的……但我从未遇到过。 在运行时变得足够长以准确测量之前,我不得不将我的测试加入到1 亿次迭代。

一般来说,我会说你应该尽量避免担心是否推迟。

IEnumerable的流执行性质有其优点。 确实如此 – 有时它是不利的,但我建议只是总是专门处理那些(罕见)时间 – 要么ToList()ToArray()将其转换为适当的列表或数组。

剩下的时间,最好让它延期。 需要经常检查这似乎是一个更大的设计问题…

我的五美分。 很多时候你必须处理一个你不知道里面是什么的可枚举。

你的选择是:

  • 在使用它之前把它变成一个列表但是很有可能你遇到了麻烦
  • 按原样使用它,你可能会遇到各种延迟执行有趣的事情,你又遇到了麻烦

这是一个例子:

 [TestClass] public class BadExample { public class Item { public String Value { get; set; } } public IEnumerable SomebodysElseMethodWeHaveNoControlOver() { var values = "at the end everything must be in upper".Split(' '); return values.Select(x => new Item { Value = x }); } [TestMethod] public void Test() { var items = this.SomebodysElseMethodWeHaveNoControlOver(); foreach (var item in items) { item.Value = item.Value.ToUpper(); } var mustBeInUpper = String.Join(" ", items.Select(x => x.Value).ToArray()); Trace.WriteLine(mustBeInUpper); // output is in lower: at the end everything must be in upper Assert.AreEqual("AT THE END EVERYTHING MUST BE IN UPPER", mustBeInUpper); // <== fails here } } 

所以没有办法摆脱它,但是那个:在现成的基础上完全迭代它一次。

对于即时和延迟执行方案,使用相同的IEnumerable接口显然是一个糟糕的设计选择。 这两者之间必须有明确的区别,以便从名称或通过检查财产来确定是否推迟了可枚举。

提示:在您的代码中考虑使用IReadOnlyCollection而不是普通的IEnumerable ,因为除此之外您还可以获得Count属性。 这种方式你肯定知道它不是无穷无尽的你可以把它变成一个列表没问题。

有关扩展结果视图的消息将评估该集合是为所有IEnumerable对象呈现的标准消息。 我不确定是否有任何万无一失的检查IEnumerable是否被推迟的方法,主要是因为即使yield也是延期的。 绝对确保不延期的唯一方法是接受ICollectionIList

绝对有可能手动实现一个懒惰的IEnumerator ,因此没有“完全通用”的方法。 我要记住的是:如果我在枚举与之相关的内容时更改列表中的内容,请始终在foreach之前调用ToArray()

这是对延迟执行的有趣反应 – 大多数人认为它是积极的,因为它允许您转换数据流而无需缓冲所有内容。

您建议的测试将不起作用,因为没有理由为什么迭代器方法不能在两次连续尝试中产生与其第一个对象相同的引用对象实例。

 IEnumerable Names() { yield return "Fred"; } 

这将每次返回相同的静态字符串对象,作为序列中的唯一项目。

由于您无法可靠地检测从迭代器方法返回的编译器生成的类,因此您必须执行相反的操作:检查一些众所周知的容器:

 public static IEnumerable ToNonDeferred(this IEnumerable source) { if (source is List || source is T[]) // and any others you encounter return source; return source.ToArray(); } 

通过返回IEnumerable ,我们将集合保持为只读,这很重要,因为我们可能会获得副本或原始集合。