为什么在递归方法中使用IEnumerable <t>会比使用List <t>慢得多?

时间:2016-03-06 01:22:02

标签: c# .net recursion

我使用递归方法遍历项目树并将其子项添加到平面集合中:

public class Thing
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? ParentId { get; set; }
}

void Main()
{
    var sampleData = new List<Thing>
    {
        new Thing { Id = 1,  Name = "root1", ParentId = null },
        new Thing { Id = 2,  Name = "2", ParentId = 1 },
        new Thing { Id = 3,  Name = "3", ParentId = 1 },
        new Thing { Id = 4,  Name = "4", ParentId = 2 },
        new Thing { Id = 5,  Name = "5", ParentId = 2 },
        new Thing { Id = 6,  Name = "6", ParentId = 2 },
        new Thing { Id = 7,  Name = "7", ParentId = 6 },
        new Thing { Id = 8,  Name = "8", ParentId = 7 },
        new Thing { Id = 9,  Name = "9", ParentId = 8 },
        new Thing { Id = 10,  Name = "10", ParentId = 9 },
        new Thing { Id = 11,  Name = "11", ParentId = 10 },
        new Thing { Id = 12,  Name = "12", ParentId = 11 },
        new Thing { Id = 13,  Name = "13", ParentId = 12 },
        new Thing { Id = 14,  Name = "14", ParentId = 13 },
        new Thing { Id = 15,  Name = "root15", ParentId = null }
    };

    var subThings = new HashSet<Thing>();
    var stopWatch = new Stopwatch();
    stopWatch.Start();
    //AddSubThings(subThings, sampleData, new List<int> { 1 });
    AddSubThingsUsingList(subThings, sampleData, new List<int> { 1 });
    stopWatch.Elapsed.Dump();
    subThings.Dump();
}

private void AddSubThings(HashSet<Thing> resultThings, IEnumerable<Thing> sourceThings, IEnumerable<int> parentIds)
{
    if (!sourceThings.Any() || !parentIds.Any())
    {
        return;
    }
    var subThings = sourceThings.Where(st => st.ParentId.HasValue && parentIds.Contains(st.ParentId.Value));
    resultThings.UnionWith(subThings);
    AddSubThings(resultThings, sourceThings.Except(subThings), subThings.Select(st => st.Id));
}

private void AddSubThingsUsingList(HashSet<Thing> resultThings, List<Thing> sourceThings, List<int> parentIds)
{
    if (!sourceThings.Any() || !parentIds.Any())
    {
        return;
    }
    var subThings = sourceThings.Where(st => st.ParentId.HasValue && parentIds.Contains(st.ParentId.Value));
    resultThings.UnionWith(subThings);
    AddSubThingsUsingList(resultThings, sourceThings.Except(subThings).ToList(), subThings.Select(st => st.Id).ToList());
}

当我使用AddSubThings方法时,处理大约需要90秒。但是,如果我使用AddSubThingsUsingList方法,它甚至不需要一秒钟。这是为什么?

2 个答案:

答案 0 :(得分:3)

问题是因为您从subThings创建sourceThings就像这样

var subThings = sourceThings.Where(
    st => st.ParentId.HasValue && parentIds.Contains(st.ParentId.Value));

然后将以下sourceThings传递给递归调用。

sourceThings.Except(subThings)

相当于

sourceThings.Except(
    sourceThings.Where(
        st => st.ParentId.HasValue && parentIds.Contains(st.ParentId.Value)))

迭代时的查询必须两次迭代原始列表。对于每个递归调用,查询将建立并且需要迭代原始列表2 ^ n次,其中n是递归级别。并且您的查询正在由AnyHashSet.UnionWith调用进行迭代,这意味着它更像是2 ^(n + 1)。

另一个在传递查询之前立即迭代查询,从而避免了这个加倍的问题。

您可以将以下内容传递给sourceThings的递归调用,以使其更快,因为它不会在每次递归调用时将原始列表的所需迭代加倍。

sourceThings.Where(st => !st.ParentId.HasValue || !parentIds.Contains(st.ParentId.Value))

答案 1 :(得分:0)

确定。这有点复杂。

IEnumerable上的操作是惰性的,即在您需要结果之前不会执行它们。现在,当您将sourceThinssubThings传递给AddSubThings时,您还没有发送物化的集合,您所做的就是您已经定义了如何从原List s。

现在,当方法以递归方式调用自身时,它会对收到的数据添加更多过滤和选择。

当您致电Any()时,将调用所有这些选择和过滤层。

另一方面,对于List s,在调用WhereExceptSelect之后,事情就会实现,因为您致电ToList }。