我最近了解到.NET的LINQ实现创建的对象对于特定的枚举类型效率低下。
看看这段代码:
public class DummyCollection : ICollection<int>
{
public IEnumerator<int> GetEnumerator()
{
throw new Exception();
}
public int Count
{
get
{
return 10;
}
}
//some more interface methods
}
基本上,DummyCollection的实例大小为10,但如果实际枚举则抛出异常。
现在在这里:
var d = new DummyCollection();
Console.WriteLine(d.Count());
打印出10没有错误,但是这段代码:
var l = d.Select(a=> a);
Console.WriteLine(l.Count());
抛出异常,尽管说l的大小也是10也是微不足道的(因为Select提供了1对1的映射)。这基本上意味着,当检查Ienumerable的长度时,输入可能是Select-wrapped Collection,从而将计算时间从O(1)扩展到惊人的O(n)(可能更糟,如果选择功能特别麻烦。)
我知道你在请求LINQ的泛型时会牺牲效率,但这似乎是一个很难解决的问题。我在网上查了一下,无法找到解决这个问题的人。有没有办法绕过这个缺点?有人在调查这个吗?有人修这个吗?这只是一个边缘情况,这不是一个大问题吗?任何见解都表示赞赏。
答案 0 :(得分:6)
您可以看到Count()
扩展方法的实施方式here。基本上是这样的:
public static int Count<TSource>(this IEnumerable<TSource> source)
{
if (source == null) throw Error.ArgumentNull("source");
ICollection<TSource> collectionoft = source as ICollection<TSource>;
if (collectionoft != null) return collectionoft.Count;
ICollection collection = source as ICollection;
if (collection != null) return collection.Count;
int count = 0;
using (IEnumerator<TSource> e = source.GetEnumerator()) {
checked {
while (e.MoveNext()) count++;
}
}
return count;
}
正如您所看到的,方法检查首先是source
类型为ICollection<TSource>
或ICollection
,如果是这种情况,那么就不需要迭代计算元素,只需返回Count
属性。
在您的第一种情况下,Count
属性被称为返回10
,并且永远不会调用GetEnumerator()
方法。
当您使用Select()
方法时,您要将该集合包装到另一种不是ICollection
的类型中(在上面的链接中,您还可以看到Select()
实现)因此迭代是必要的。
在第二种情况下,当您致电Count()
时,系统会调用您的GetEnumerator()
方法并抛出异常。
答案 1 :(得分:2)
IEnumerable<T>
没有Count
的概念。这存在于实现中,(除了这里和那里的奇怪捷径之外)在LINQ to Objects中没有任何作用。如果您使用IEnumerable<T>
投影ICollection<T>
的实施(例如Select
),那么唯一真正的保证是输出将为IEnumerable<T>
...没有Count
。
LINQ应被视为处理项目序列,一次一个,只有当前和下一个项目(或序列结束)的概念。了解项目数量是一项(可能)成本高昂的操作,需要迭代所有被计算的项目,而不是在少数优化案例中。
鉴于LINQ 依赖 进行迭代而不是索引和计数意味着当您尝试迭代它时出现错误的IEnumerable
将需要一些超级奇怪的特殊套管飞。对我来说,这不是一个非常有用的用例。