Linq中的ToList方法

本文关键字:方法 ToList 中的 Linq | 更新日期: 2023-09-27 18:00:39

如果我没有错,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()方法迭代两次。

我现在正在进行的项目已经广泛使用了Lists,我看到有很多这样的ToList()和其他方法的使用,如果我将list变量作为IEnumerable并移除.ToList()并将其进一步用作IEnumerable,可以像上面一样做得更好。

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

Linq中的ToList方法

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

这取决于您的代码。大多数情况下,使用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()将从Where()调用2n次,从ToList()调用n次。

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被调用的次数,我们必须同时实现我们的自定义枚举器。请注意,在MoveNext中,我们的程序中有一个静态计数器。

公共类TestEnumerator:IEnumerator{public Item 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 to SQL/EF,那么在这种情况下,当调用.ToList时,只会有一次迭代,它在内部调用.GetEnumerator。然后,查询表达式被解析为TSQL并传递给数据库。然后对得到的行进行迭代(一次)并将其添加到列表中。

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

首先,你为什么问我?自己量一下看。

也就是说,WhereSelectOrderBy和其他LINQ IEnumerable扩展方法通常被实现为尽可能懒惰(经常使用yield关键字)。这意味着除非迫不得已,否则他们不会处理数据

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

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

然而,在调用LINQ方法时会有一些初始开销,因此传统的方法通常会更快,但LINQ方法IMHO的可组合性和易用性远远弥补了这一点。

如果您想了解这些方法是如何实现的,可以从Microsoft参考源中获得参考。