在foreach中,List<T>与IEnumerable<T>的区别

5

我试图在foreach循环中使用OrderBy对一些列表进行排序,但是不知何故,它们在循环外部无法保持排序顺序。下面是一些简化的代码和注释,以突显问题:

public class Parent
{
    // Other properties...
    public IList<Child> Children { get; set; }
}

public IEnumerable<Parent> DoStuff()
{
    var result = DoOtherStuff() // Returns IEnumerable<Parent>
        .OrderByDescending(SomePredicate) 
        .ThenBy(AnotherPredicate); // This sorting works as expected in the return value.

    foreach (Parent parent in result)
    {
        parent.Children = parent.Children.OrderBy(YetAnotherPredicate).ToList();
        // When I look at parent.Children here in the debugger, it's sorted properly.
    }

    return result;
    // When I look at the return value, the Children are not sorted.
}

然而,当我这样赋值result时:

var result = DoOtherStuff()
    .OrderByDescending(SomePredicate)
    .ThenBy(AnotherPredicate)
    .ToList(); // <-- Added ToList here

如果返回值在每个父级中都按照子级正确排序,则具有正确排序的Children。

List<T>IEnumerable<T>在foreach循环中的行为有何不同?

由于将result转换为List可以解决foreach循环中排序问题,因此似乎存在某种差异。在使用foreach进行迭代时,第一个代码片段创建了一个复制每个元素的迭代器(因此我的更改应用于副本而不是result中的原始对象),而使用ToList()则使枚举器给出指针。发生了什么?

3个回答

7
区别在于一个是可以产生一组Parent对象的表达式,而另一个是Parent对象的列表。每次使用该表达式时,它将使用来自DoOtherStuff的原始结果,然后对其进行排序。在您的情况下,这意味着它将创建一组新的Parent对象(因为它们显然不保留以前使用的子项)。这意味着当您循环遍历对象并对子项进行排序时,这些对象将被丢弃。当您再次使用该表达式返回结果时,它将创建一组新的对象,其中子项自然按照原始顺序排列。

@user2864740:我明白你的意思了。我已经更正了答案。 - Guffa
这是正确的...可以通过在DoOtherStuff()中设置断点轻松检查。每次迭代枚举器(IEnumerable<Parent>)时,都会调用DoOtherStuff。调用ToList()执行枚举器并将结果放入列表中。遍历列表不会触发DoOtherStuff()中的断点。 - Mick
@Mick:DoOtherStuff方法实际上不会再次被调用,但是方法内的代码可能会再次执行。该方法返回一个枚举器,当它被重新创建时,创建它的代码将再次运行。 - Guffa
是的,如果lambda函数用于创建枚举并针对内存集合执行,则有可能在其中放置断点。如果使用的是Entity Framework,则会被解释而不是执行。您可以在SQL Profiler中每次查看执行的查询。 - Mick
@Guffa 我来晚了,但还是感谢您的回复。那些年我没有接受您的回答,因为当时我不知道您在说什么,但现在我更年长更明智了,更好地理解了 IEnumerable<T>System.Linq 的工作原理,这个答案非常准确。 - wlyles

2

以下是可能添加到Guffa的答案中的示例代码:

class Parent { public List<string> Children; }

"Parent"的可枚举对象,每次迭代时都会创建新的"Parent"对象:

var result = Enumerable.Range(0, 10)
      .Select(_ => new Parent { Children = new List<sting>{"b", "a"});

现在,使用 foreach 的首次迭代将创建10个“Parent”对象(每次循环一个),并在每次迭代结束时立即丢弃:

foreach (Parent parent in result)
{
    // sorts children of just created parent object
    parent.Children = parent.Children.OrderBy(YetAnotherPredicate).ToList();

    // parent is no longer referenced by anything - discarded and eligible for GC
}

当您再次查看result时,它将被重新迭代并创建新的一组“父”对象,因此“子项”未排序。
请注意,根据DoOtherStuff() // Returns IEnumerable<Parent>的实现方式,结果可能会有所不同。即DoOtherStuff()可以从某个缓存集合返回现有项目的集合:
 List<Parent> allMyParents = ...; 

 IEnumerable<Parent> DoOtherStuff()
 {
      return allMyParents.Take(7);
 }

现在,每次迭代result都会给你一个新的集合,但集合中的每个项目都只是来自allMyParents列表中的一个项目 - 因此修改“Children”属性会改变allMyParents中的实例,并且修改会保留下来。

-1
备注 ToList(IEnumerable) 方法强制立即查询评估并返回包含查询结果的列表。您可以将此方法附加到查询中,以获取查询结果的缓存副本。
如果省略 ToList(),则不会评估查询...您的调试器可能会为您执行此操作,但这只是我的猜测。
来源:https://msdn.microsoft.com/zh-cn/library/bb342261(v=vs.110).aspx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接