通过Reflector查看 System.Linq.Enumerable 时,我注意到默认迭代器用于选择和其中扩展方法 - WhereSelectArrayIterator - 未实现 ICollection 界面。如果我正确读取代码,这会导致其他一些扩展方法,例如 Count()和 ToList()执行得更慢:
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
// code above snipped
if (source is List<TSource>)
{
return new WhereSelectListIterator<TSource, TResult>((List<TSource>) source, null, selector);
}
// code below snipped
}
private class WhereSelectListIterator<TSource, TResult> : Enumerable.Iterator<TResult>
{
// Fields
private List<TSource> source; // class has access to List source so can implement ICollection
// code below snipped
}
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
public List(IEnumerable<T> collection)
{
ICollection<T> is2 = collection as ICollection<T>;
if (is2 != null)
{
int count = is2.Count;
this._items = new T[count];
is2.CopyTo(this._items, 0); // FAST
this._size = count;
}
else
{
this._size = 0;
this._items = new T[4];
using (IEnumerator<T> enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
this.Add(enumerator.Current); // SLOW, CAUSES ARRAY EXPANSION
}
}
}
}
}
我对结果进行了测试,证实了我的怀疑:
ICollection:2388.5222 ms
IEnumerable:3308.3382 ms
这是测试代码:
// prepare source
var n = 10000;
var source = new List<int>(n);
for (int i = 0; i < n; i++) source.Add(i);
// Test List creation using ICollection
var startTime = DateTime.Now;
for (int i = 0; i < n; i++)
{
foreach(int l in source.Select(k => k)); // itterate to make comparison fair
new List<int>(source);
}
var finishTime = DateTime.Now;
Response.Write("ICollection: " + (finishTime - startTime).TotalMilliseconds + " ms <br />");
// Test List creation using IEnumerable
startTime = DateTime.Now;
for (int i = 0; i < n; i++) new List<int>(source.Select(k => k));
finishTime = DateTime.Now;
Response.Write("IEnumerable: " + (finishTime - startTime).TotalMilliseconds + " ms");
我是否遗漏了某些内容或将在未来版本的框架中修复此问题?
感谢您的想法。
答案 0 :(得分:5)
LINQ to Objects使用一些技巧来优化某些操作。例如,如果将两个.Where
语句链接在一起,则谓词将合并为单个WhereArrayIterator
,因此之前的谓词可以被垃圾收集。同样,Where
后跟Select
将创建WhereSelectArrayIterator
,将组合谓词作为参数传递,以便可以对原始WhereArrayiterator
进行垃圾回收。因此,WhereSelectArrayIterator
不仅要跟踪selector
,还要跟踪它可能会或可能不会基于的合并predicate
。
source
字段仅跟踪给定的初始列表。由于谓词,迭代结果并不总是与source
具有相同数量的项目。由于LINQ旨在进行延迟评估,因此不应提前评估source
与predicate
之间的关联,以便在有人最终调用.Count()
时可以节省时间。这会导致性能损失与手动调用.ToList()
一样,如果用户通过多个Where
和Select
子句运行它,您最终会构建多个列表不必要的。
可以重构LINQ to Objects以创建在SelectArrayIterator
直接在数组上调用时使用的Select
吗?当然。它会提高性能吗?一点点。费用是多少?减少代码重用意味着需要额外的代码来维护和测试前进。
因此我们得到绝大多数“为什么语言/平台X没有功能Y”问题的关键:每个功能和优化都有一些与之相关的成本,甚至微软也没有无限的资源。就像其他所有公司一样,他们会进行判断调用,以确定运行代码的频率,该代码在数组上执行Select
,然后在其上调用.ToList()
,以及是否更快地运行值得编写和维护LINQ包中的另一个类。