【问题标题】:ToList method in LinqLinq 中的 ToList 方法
【发布时间】:2013-02-22 15:22:11
【问题描述】:

如果我没记错的话,ToList() 方法会迭代提供的集合的每个元素,并将它们添加到 List 的新实例并返回此实例。假设一个示例

//using linq
list = Students.Where(s => s.Name == "ABC").ToList();

//traditional way
foreach (var student in Students)
{
  if (student.Name == "ABC")
    list.Add(student);
}

我认为传统的方式更快,因为它只循环一次,而上面的 Linq 对 Where 方法迭代两次,然后对 ToList() 方法迭代。

我正在从事的项目现在广泛使用列表,我看到有很多这样的 ToList() 和其他方法的使用,如果我采用 list 变量作为 IEnumerable 并删除 .ToList() 并将其进一步用作 IEnumerable。

这些事情对性能有影响吗?

【问题讨论】:

  • Where 视为循环中的if 子句,而不是循环。实际上Where会在ToList枚举序列时使用。

标签: c# performance linq


【解决方案1】:

这些事情对性能有影响吗?

这取决于您的代码。大多数情况下,使用 LINQ 确实会对性能造成很小的影响。在某些情况下,这种影响对您来说可能很重要,但只有在您知道 LINQ 对您来说太慢时才应该避免使用 LINQ(即,如果分析您的代码显示 LINQ 是您的代码运行缓慢的原因)。

但您说得对,过于频繁地使用ToList() 会导致严重的性能问题。仅在必要时才应致电ToList()。请注意,在某些情况下,添加 ToList() 可以大大提高性能(例如,每次迭代时从数据库加载集合时)。

关于迭代次数:这取决于您所说的“迭代两次”到底是什么意思。如果您计算在某个集合上调用MoveNext() 的次数,那么是的,使用Where() 这种方式会导致迭代两次。操作顺序是这样的(为了简化,我假设所有项目都符合条件):

  1. Where() 被调用,暂时没有迭代,Where() 返回一个特殊的枚举。
  2. ToList() 被调用,对从Where() 返回的枚举调用MoveNext()
  3. Where() 现在在原始集合上调用 MoveNext() 并获取值。
  4. Where() 调用你的谓词,它返回true
  5. ToList() 调用的MoveNext() 返回,ToList() 获取值并将其添加到列表中。

这意味着如果原始集合中的所有 n 个项目都符合条件,MoveNext() 将被调用 2n 次,n em> 次来自Where()n 次来自ToList()

【讨论】:

  • 很好的描述(只要这是 LINQ to Objects)。 LINQ to SQL/EF 只会遍历生成的数据表。
  • @JimWooley 是的,我认为这是 LINQ to objects,这似乎是问题所在(尽管它没有明确说明)。
  • 无论是 LINQ to objects 还是 LINQ to SQL/EF,这都是错误的。如果您对原因感兴趣,可以查看我的答案。 MoveNext 没有被调用 2n 次。只进行了一次迭代。
【解决方案2】:
var list = Students.Where(s=>s.Name == "ABC"); 

这只会创建一个查询并且在使用查询之前不会循环元素。通过调用 ToList() 将首先执行查询,因此只循环你的元素一次。

List<Student> studentList = new List<Student>();
var list = Students.Where(s=>s.Name == "ABC");
foreach(Student s in list)
{
    studentList.add(s);
}

这个例子也只会迭代一次。因为它只用过一次。请记住,该列表将在每次调用时迭代所有学生。不仅仅是那些名字是 ABC 的学生。因为它是一个查询。

为了后面的讨论,我做了一个测试示例。也许它不是 IEnumable 的最佳实现,但它做了它应该做的事情。

首先我们有我们的清单

public class TestList<T> : IEnumerable<T>
{
    private TestEnumerator<T> _Enumerator;

    public TestList()
    {
        _Enumerator = new TestEnumerator<T>();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _Enumerator;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    internal void Add(T p)
    {
        _Enumerator.Add(p);
    }
}

由于我们想计算调用 MoveNext 的次数,我们必须实现我们的自定义枚举器 aswel。在 MoveNext 中观察,我们的程序中有一个静态计数器。

公共类 TestEnumerator : IEnumerator { 公共项目 FirstItem = null; public Item CurrentItem = null;

    public TestEnumerator()
    {
    }

    public T Current
    {
        get { return CurrentItem.Value; }
    }

    public void Dispose()
    {

    }

    object System.Collections.IEnumerator.Current
    {
        get { throw new NotImplementedException(); }
    }

    public bool MoveNext()
    {
        Program.Counter++;
        if (CurrentItem == null)
        {
            CurrentItem = FirstItem;
            return true;
        }
        if (CurrentItem != null && CurrentItem.NextItem != null)
        {
            CurrentItem = CurrentItem.NextItem;
            return true;
        }
        return false;
    }

    public void Reset()
    {
        CurrentItem = null;
    }

    internal void Add(T p)
    {
        if (FirstItem == null)
        {
            FirstItem = new Item<T>(p);
            return;
        }
        Item<T> lastItem = FirstItem;
        while (lastItem.NextItem != null)
        {
            lastItem = lastItem.NextItem;
        }
        lastItem.NextItem = new Item<T>(p);
    }
}

然后我们有一个自定义项目,它只是包装我们的价值

public class Item<T>
{
    public Item(T item)
    {
        Value = item;
    }

    public T Value;

    public Item<T> NextItem;
}

为了使用实际代码,我们创建了一个包含 3 个条目的“列表”。

    public static int Counter = 0;
    static void Main(string[] args)
    {
        TestList<int> list = new TestList<int>();
        list.Add(1);
        list.Add(2);
        list.Add(3);

        var v = list.Where(c => c == 2).ToList(); //will use movenext 4 times
        var v = list.Where(c => true).ToList();   //will also use movenext 4 times


        List<int> tmpList = new List<int>(); //And the loop in OP question
        foreach(var i in list)
        {
            tmpList.Add(i);
        }                                    //Also 4 times.
    }

结论呢?它如何影响性能? 在这种情况下,MoveNext 被调用 n+1 次。不管我们有多少物品。 而且 WhereClause 也无所谓,他仍然会运行 MoveNext 4 次。因为我们总是在我们的初始列表上运行我们的查询。 我们将受到的唯一性能影响是实际的 LINQ 框架及其调用。实际制作的循环将是相同的。

在有人问为什么它是 N+1 次而不是 N 次之前。这是因为他最后一次没有元素时返回 false。使其成为元素的数量+列表的结尾。

【讨论】:

  • 如果比较这两个样本,使用 LINQ 的样本确实会迭代(即为每个元素调用 MoveNext())集合两次:一次在 Where() 中,一次在 ToList() 中。 (虽然第二次,它可能是一个较小的集合。)这通常不会对性能产生太大影响,但它可能会产生影响。
  • 不,因为正如我提到的,它只是一个未执行的查询。当您实际使用它时,它会首先执行。类似于你使用 foreach(var v in list) 之后的样子。它仍然只会运行一次。
  • 是的,Where() 仅在您迭代结果时执行。但是当你这样做时,Where() 会迭代原始集合,ToList() 会迭代从Where() 返回的集合。所以你确实迭代了两次。
  • @svick:Where 不会迭代,只有 ToList 会。
  • @svick 你能给我一个链接吗?我可以在这里阅读更多信息。我没有告诉你你错了。但如果我错了,我真的很想知道..
【解决方案3】:

要完全回答这个问题,这取决于实现。如果您正在谈论 LINQ to SQL/EF,那么在这种情况下,当调用 .ToList 时将只有一次迭代,它在内部调用 .GetEnumerator。然后将查询表达式解析为 TSQL 并传递给数据库。然后将生成的行迭代(一次)并添加到列表中。

在 LINQ to Objects 的情况下,也只有一次数据传递。在 where 子句中使用 yield return 在内部设置了一个状态机,该状态机跟踪进程在迭代中的位置。 Where 不进行完整迭代,创建一个临时列表,然后将这些结果传递给查询的其余部分。它只是确定一个项目是否符合标准,并且只传递那些匹配的项目。

【讨论】:

  • 当您使用投影时会怎样,例如 aList.Select(i =&gt; new B(i.Name)).ToList(); 它是否使用 LINQ to Objects 循环两次?
  • @codewise 否,iterator 在 ToList 使用的单个 foreach 迭代中产生新值。
【解决方案4】:

首先Why are you even asking me?自己量一下看看。

也就是说,WhereSelectOrderBy 和其他 LINQ IEnumerable 扩展方法通常都尽可能地实现惰性(yield 关键字经常使用)。这意味着除非必须,否则他们不会处理数据。从你的例子:

var list = Students.Where(s => s.Name == "ABC");

不会执行任何操作。即使Students 是一千万个对象的列表,这也会立即返回。在某个地方实际请求结果之前,根本不会调用谓词,这实际上是ToList() 所做的:它说“是的,结果 - 所有这些 - 都是立即需要的”。

然而,调用 LINQ 方法会产生一些初始开销,因此传统方法通常会更快,但 LINQ 方法的可组合性和易用性(恕我直言)足以弥补这一点.

如果你想看看这些方法是如何实现的,可以参考Microsoft Reference Sources

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多