在foreach中的Enumerable.Where中无法访问C#对象属性

时间:2015-05-04 16:11:23

标签: c# foreach

找到一个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];
...

我找不到它为什么会这样的答案。 谁能解释一下呢?

2 个答案:

答案 0 :(得分:2)

这是由linq查询的惰性引起的。 Linq查询将尽可能“实现”,以避免做不必要的工作。

children是非物化IEnumerable<T>。它实际上不会填充元素。 parentparentId之间存在显着差异,用于您的两个.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对象。但是,我更喜欢你现有的解决方案,因为如果你不需要同时扩展所有这些孩子,它会更有效。

好消息是this problem is fixed for C# 5