我总是假设如果我在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<T>
作为班级成员。通常,LINQ查询的结果被分配给这样的IEnumerable<T>
。通常对我来说这不会引起问题,但我最近在我的代码中找到了一些不仅仅是性能问题的地方。
在尝试检查我犯了这个错误的地方时,我想我可以查看特定的IEnumerable<T>
是否属于IQueryable
类型。我想这会告诉我收藏是否“延期”。事实证明,上面的Select运算符创建的枚举器属于System.Linq.Enumerable+WhereSelectArrayIterator``[System.String,System.String]
类型,而不是IQueryable
。
我使用Reflector来查看此接口继承的内容,并且结果表明不会继承任何表明它是“LINQ”的内容 - 因此无法根据集合类型进行测试。
我现在非常高兴地将.ToArray()
放在任何地方,但是我想有一个机制来确保将来不会发生这个问题。 Visual Studio似乎知道如何做到这一点,因为它给出了一条关于“扩展结果视图将评估集合”的消息。
我想出的最好的是:
bool deferred = !object.ReferenceEquals(randomNumberOfAnimals.First(),
randomNumberOfAnimals.First());
编辑这仅适用于使用“选择”创建新对象且不是通用解决方案的情况。不管怎样我都不推荐它!这是一个解决方案的一点点舌头。
答案 0 :(得分:17)
延迟执行LINQ困扰了很多人,你并不孤单。
我采取的避免此问题的方法如下:
方法的参数 - 使用IEnumerable<T>
,除非需要更具体的界面。
局部变量 - 通常在我创建LINQ的时候,所以我会知道是否可以进行延迟评估。
班级成员 - 永远不要使用IEnumerable<T>
,始终使用List<T>
。并且总是让它们变得私密。
属性 - 使用IEnumerable<T>
,并在setter中转换为存储空间。
public IEnumerable<Person> People
{
get { return people; }
set { people = value.ToList(); }
}
private List<People> people;
虽然有理论上这种方法不起作用,但我还没有遇到过这种情况,而且自从Beta版以来我一直热衷于使用LINQ扩展方法。
顺便说一句:我很好奇为什么你使用ToArray();
而不是ToList();
- 对我来说,列表有更好的API,并且(几乎)没有性能成本。
更新:一些评论者正确地指出阵列具有理论上的性能优势,因此我将上述声明修改为“......几乎没有性能成本。”
更新2 :我编写了一些代码,对数组和列表之间的性能差异进行了一些微基准测试。在我的笔记本电脑上,在我的特定基准测试中,每次访问的差异大约为5ns(即 nano 秒)。我想有些情况下每个循环节省5ns是值得的......但我从未遇到过。在运行时变得足够长以准确测量之前,我不得不将我的测试加到100 百万次迭代。
答案 1 :(得分:7)
总的来说,我会说你应该尽量避免担心它是否会推迟。
IEnumerable<T>
的流执行性质有一些优点。这是事实 - 有时候它是不利的,但我建议只是总是专门处理那些(罕见)时间 - 要么ToList()
或ToArray()
将其转换为适当的列表或数组。
其余的时间,最好让它延期。需要经常检查这似乎是一个更大的设计问题......
答案 2 :(得分:3)
您的选择是:
以下是一个例子:
[TestClass]
public class BadExample
{
public class Item
{
public String Value { get; set; }
}
public IEnumerable<Item> 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<T>
而不是普通IEnumerable<T>
,因为除此之外,您还会获得Count
属性。这种方式你肯定知道它不是无穷无尽的你可以把它变成一个没有问题的列表。
答案 3 :(得分:1)
有关展开结果视图的消息将评估该集合是为所有IEnumerable
个对象呈现的标准消息。我不确定是否有任何万无一失的方法来检查IEnumerable
是否被推迟,主要是因为即使yield
被推迟。绝对确保不延迟的唯一方法是接受ICollection
或IList<T>
。
答案 4 :(得分:1)
绝对有可能手动实现一个懒惰的IEnumerator<T>
,因此没有“完全通用”的方法。我要记住的是:如果我在列出与列表相关的内容时更改列表中的内容,请始终在ToArray()
之前调用foreach
。
答案 5 :(得分:1)
这是对延迟执行的有趣反应 - 大多数人认为它是积极的,因为它允许您转换数据流而无需缓冲所有内容。
您建议的测试将不起作用,因为没有理由为什么迭代器方法在两次连续尝试中不能产生与第一个对象相同的引用对象实例。
IEnumerable<string> Names()
{
yield return "Fred";
}
每次都会返回相同的静态字符串对象,作为序列中的唯一项目。
由于您无法可靠地检测从迭代器方法返回的编译器生成的类,因此您必须执行相反的操作:检查一些众所周知的容器:
public static IEnumerable<T> ToNonDeferred(this IEnumerable<T> source)
{
if (source is List<T> || source is T[]) // and any others you encounter
return source;
return source.ToArray();
}
通过返回IEnumerable<T>
,我们会将该集合保持为只读,这很重要,因为我们可能会收到副本或原件。