找到一个c#head-scratcher。在foreach循环中,直接在Enumerable.Where中使用parent.Id属性不起作用。首先将它放在一个变量中。在Select语句中直接访问parent.Id没有问题。
List<Person> people = new List<Person>() {
new Person() { Id = 1, name = "John", parentId = null },
new Person() { Id = 2, name = "Sarah", parentId = null },
new Person() { Id = 3, name = "Daniel", parentId = 1 },
new Person() { Id = 4, name = "Peter", parentId = 1 }
};
List<object> peopleTree = new List<object>();
var parents = people.Where(p => !p.parentId.HasValue);
foreach (Person parent in parents)
{
int parentId = parent.Id;
var children = people
//.Where(p => p.parentId.Equals(parentId)) //This works, is able to find the children
.Where(p => p.parentId.Equals(parent.Id)) //This does not work, no children for John
.Select(p => new { Id = p.Id, Name = p.name, pId = parent.Id }); //pId set correctly
peopleTree.Add(new
{
Id = parent.Id,
Name = parent.name,
Children = children
});
}
或者,如果我使用for循环并将parent放在变量中,我可以直接在Where语句中访问parent.Id属性。
var parents = people.Where(p => !p.parentId.HasValue).ToArray();
for (int idx = 0; idx < parents.Count(); idx++)
{
var parent = parents[idx];
...
我找不到它为什么会这样的答案。 谁能解释一下呢?
答案 0 :(得分:2)
这是由linq查询的惰性引起的。 Linq查询将尽可能“实现”,以避免做不必要的工作。
children
是非物化IEnumerable<T>
。它实际上不会填充元素。 parent
和parentId
之间存在显着差异,用于您的两个.Where()
调用。 parent
仅声明一次,但parentId
在循环中作用域,因此有效地多次声明。在children
最终实现时,parent
已更改了值。它将引用parents
中的最后一个元素,这不是您的意图。
你可以像这样强迫进行评估。
var children = people
.Where(p => p.parentId.Equals(parent.Id))
.Select(p => new { Id = p.Id, Name = p.name, pId = parent.Id })
.ToArray(); <---- this forces materialization
答案 1 :(得分:1)
问题出现在以这样开头的声明中:
var children = people ...
此语句不会将其导入实际存储值的集合中......它会生成一个IEnumerable对象,该对象知道如何迭代集合。该对象使用的指令恰好引用了循环中的parent
变量。该变量被Enumerable捕获为称为closure的东西。稍后,当您实际使用Enumerable对象访问项目时,它会回顾该parent
变量。
这就是诀窍:有一个一个 parent
变量,它通过原始循环进行每次迭代变异。在循环结束时,parents
集合中的所有项目都使用相同的parent
对象。将parent.Id
值复制到循环内的变量可以解决问题,因为您现在正在通过循环每次迭代处理闭包的新变量。
您还可以通过在前面指示的语句末尾使用.ToList()
调用来解决此问题,以便在仍在循环内部时评估Enumerable对象。但是,我更喜欢你现有的解决方案,因为如果你不需要同时扩展所有这些孩子,它会更有效。