如果我没有错,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 < / strong>变量为 IEnumerable 并删除.ToList()并将其进一步用作IEnumerable。
这些事情对性能有影响吗?
答案 0 :(得分:10)
这些事情对性能有影响吗?
这取决于您的代码。大多数情况下,使用LINQ确实会导致性能下降。在某些情况下,这个命中对您来说可能很重要,但是只有当您知道它对您来说太慢时才应该避免使用LINQ(即,如果对代码进行分析表明LINQ是您的代码速度慢的原因)。
但是你经常使用ToList()
会导致严重的性能问题。您必须在必要时致电ToList()
。请注意,在某些情况下,添加ToList()
可以大大提高性能(例如,每次迭代时从数据库加载集合时)。
关于迭代次数:它取决于“迭代两次”究竟是什么意思。如果计算某些集合上调用MoveNext()
的次数,则是,使用Where()
这种方式会导致迭代两次。操作顺序如下(为了简化,我将假设所有项都符合条件):
Where()
被调用,暂时没有迭代,Where()
返回一个特殊的可枚举。ToList()
,在MoveNext()
返回的枚举上调用Where()
。Where()
现在调用原始集合上的MoveNext()
并获取值。Where()
调用您的谓词,返回true
。MoveNext()
调用的ToList()
返回,ToList()
获取值并将其添加到列表中。这意味着如果原始集合中的所有 n 项都符合条件,MoveNext()
将被称为2 n 次, n 来自Where()
的{{1}}次和来自ToList()
的 n 次。
答案 1 :(得分:4)
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中观察我们的程序中有一个静态的计数器。
public class 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也没关系,他仍然会运行4次MoveNext。因为我们总是在初始列表上运行查询。 我们将采取的唯一性能影响是实际的LINQ框架及其调用。制作的实际循环将是相同的。
之前有人问为什么N + 1次而不是N次。这是因为他最后一次失去元素时会返回false。使其成为元素数量+列表末尾。
答案 2 :(得分:1)
首先,Why are you even asking me?自己测量并看看。
也就是说,Where
,Select
,OrderBy
和其他LINQ IEnumerable
扩展方法通常尽可能实现为懒惰({{3}经常使用关键字)。这意味着除非必须,否则他们不会处理数据。从你的例子:
var list = Students.Where(s => s.Name == "ABC");
不执行任何操作。即使Students
是1000万个对象的列表,这也会立即返回。在某个地方实际请求结果之前,根本不会调用谓词,这实际上是ToList()
所做的:它说“是的,结果 - 所有这些都是立即需要的”。
如果您想了解这些方法的实施方式,可以参考yield
进行参考。
答案 3 :(得分:1)
要完全回答这个问题,这取决于实施情况。如果你在讨论LINQ to SQL / EF,那么在这种情况下只有一次迭代时调用.ToList,它在内部调用.GetEnumerator。然后将查询表达式解析为TSQL并传递给数据库。然后迭代(一次)生成的行并将其添加到列表中。
对于LINQ to Objects,只有一次传递数据。在where子句中使用yield return会在内部设置一个状态机,用于跟踪进程在迭代中的位置。哪里不进行完整迭代创建临时列表,然后将这些结果传递给查询的其余部分。它只是确定一个项目是否符合标准,只传递那些匹配的项目。