使用reflector我注意到System.Linq.Enumerable.Count
方法有条件优化它,以便在IEnumerable<T>
传递的情况下实际上是ICollection<T>
。如果转换成功,则Count方法不需要迭代每个元素,但可以调用ICollection的Count方法。
基于此,我开始认为IEnumerable<T>
可以像集合的只读视图一样使用,而不会出现我最初期望的基于IEnumerable<T>
的API的性能损失
当Count
是对IEnumerable<T>
语句的Select
语句的结果,但基于反映的代码时,我对ICollection
的优化是否仍然有效感兴趣case未经优化,需要迭代所有元素。
你从反射器得出相同的结论吗?缺少这种优化背后的原因是什么?我似乎在这个常见的操作中浪费了很多时间。规范是否需要即使可以在不这样做的情况下确定Count,也会评估每个元素?
答案 0 :(得分:12)
Select
的结果被懒惰地评估并不重要。 Count
始终等同于原始集合的计数,因此可以通过返回Select
中可用于短路评估Count
的特定对象来直接检索它。方法。
无法根据具有确定数量的内容(如Count()
)优化对Select
调用返回值的List<T>
方法评估的原因是它可以改变程序的含义。
传递给selector
方法的Select
函数允许具有副作用,并且必须以预定的顺序确定性地发生其副作用。
假设:
new[]{1,2,3}.Select(i => { Console.WriteLine(i); return 0; }).Count();
文档要求此代码打印
1
2
3
即使从开始开始知道计数并且可以优化,优化也会改变程序的行为。这就是为什么你无论如何都无法避免集合的枚举。这正是编译器优化在纯函数式语言中更容易的原因之一。
更新:显然,实施Select
和Count
是完全可能的,这样Select
上的ICollection<T>
仍然可以懒惰地评估,但Count()
将在O(1)中进行评估而不枚举集合。我将在不改变任何方法的界面的情况下这样做。类似的事情已经为ICollection<T>
完成了:
private interface IDirectlyCountable {
int Count {get;}
}
private class SelectICollectionIterator<TSource,TResult> : IEnumerable<T>, IDirectlyCountable {
ICollection<TSource> sequence;
Func<TSource,TResult> selector;
public SelectICollectionIterator(ICollection<TSource> source, Func<TSource,TResult> selector) {
this.sequence = source;
this.selector = selector;
}
public int Count { get { return sequence.Count; } }
// ... GetEnumerator ...
}
public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource,TResult> selector) {
// ... error handling omitted for brevity ...
if (source is ICollection<TSource>)
return new SelectICollectionIterator<TSource,TResult>((ICollection<TSource>)source, selector);
// ... rest of the method ...
}
public static int Count<T>(this IEnumerable<T> source) {
// ...
ICollection<T> collection = source as ICollection<T>;
if (collection != null) return collection.Count;
IDirectlyCountable countableSequence = source as IDirectlyCountable;
if (countableSequence != null) return countableSequence.Count;
// ... enumerate and count the sequence ...
}
这仍然会懒惰地评估Count
。如果更改基础集合,则计数将更改,并且不会缓存序列。唯一的区别是不会在selector
委托中执行副作用。
答案 1 :(得分:1)
编辑02-Feb-2010 :
正如我所看到的,至少有两种方法可以解释这个问题。
为什么
Select<T, TResult>
扩展方法何时 调用了一个类的实例 实现ICollection<T>
,而不是 返回提供a的对象Count
财产;为什么Count<T>
扩展方法不是 检查这个属性,以便 当两者提供O(1)表现时 方法是链接的?
此版本的问题不会对Linq扩展如何工作做出错误的假设,并且是一个有效的问题,因为调用ICollection<T>.Select.Count
毕竟总是会返回与ICollection<T>.Count
相同的值。这就是迈赫达德解释这个问题的方式,他对此提出了彻底的回应。
但我将问题视为要求......
如果
Count<T>
扩展方法提供O(1) 一个类的对象的性能 实施ICollection<T>
,为什么 它是否提供O(n)性能 的回报值Select<T, TResult>
扩展方法?
在这个版本的问题中,有一个错误的假设:Linq扩展方法通过一个接一个地组装小集合(在内存中)并通过IEnumerable<T>
接口公开它们来协同工作。
如果这是Linq扩展的工作方式,那么Select
方法可能如下所示:
public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
List<TResult> results = new List<TResult>();
foreach (T input in source)
results.Add(selector(input));
return results;
}
此外,如果 是Select
的实现,我认为您会发现大多数使用此方法的代码行为都相同。但这会很浪费,事实上会在某些情况下引起例外情况,例如我在原始答案中所描述的情况。
实际上,我认为Select
方法的实现更接近这样的事情:
public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
foreach (T input in source)
yield return selector(input);
yield break;
}
这是为了提供延迟评估,并解释为什么在{(1)}方法的O(1)时间内无法访问Count
属性。
换句话说,虽然Mehrdad回答了为什么Count
不是设计的问题,但Select
表现不同,我已经提出了我对为什么Select.Count
行为方式的问题的最佳答案。
原始回答:
根据Mehrdad的回答:
这并不重要 Select的结果是懒惰的评估。
我不买这个。让我解释一下原因。
对于初学者,请考虑以下两种非常相似的方法:
Select.Count
好的,这些方法有什么作用?每个人返回与用户期望的一样多的随机双打(最多public static IEnumerable<double> GetRandomsAsEnumerable(int N) {
Random r = new Random();
for (int i = 0; i < N; ++i)
yield return r.NextDouble();
yield break;
}
public static double[] GetRandomsAsArray(int N) {
Random r = new Random();
double[] values = new double[N];
for (int i = 0; i < N; ++i)
values[i] = r.NextDouble();
return values;
}
)。这两种方法是否被懒惰评估是否重要?要回答这个问题,我们来看看下面的代码:
int.MaxValue
你能猜出这两个方法调用会发生什么吗?让我免去你复制这段代码并自己测试的麻烦:
第一个变量public static double Invert(double value) {
return 1.0 / value;
}
public static void Test() {
int a = GetRandomsAsEnumerable(int.MaxValue).Select(Invert).Count();
int b = GetRandomsAsArray(int.MaxValue).Select(Invert).Count();
}
将(在可能的大量时间之后)初始化为a
(目前为2147483647)。 第二个一个int.MaxValue
很可能会被b
中断。
因为OutOfMemoryException
和其他Linq扩展方法被懒惰地评估,它们允许你做你根本做不到的事情。以上是一个相当简单的例子。但我的主要观点是对懒惰评估并不重要的断言提出异议。 Mehrdad声明Select
属性“从一开始就是真正知道并且可以被优化”实际上引发了一个问题。对于Count
方法,问题似乎很简单,但Select
并不是特别的;它返回一个Select
,就像Linq扩展方法的其余部分一样,并且这些方法“知道”其返回值的IEnumerable<T>
将需要缓存完整的集合,因此禁止延迟评价强>
出于这个原因,我必须同意其中一个原始响应者(现在答案似乎已经消失),懒惰评估确实是这里的答案。需要考虑方法副作用的想法实际上是次要的,因为无论如何这已经被确定为懒惰评估的副产品。
后记:我发表了非常自信的陈述并强调了我的观点,主要是因为我想明确我的论点是什么,而不是出于对任何其他回应的不尊重,包括Mehrdad,我认为这是有洞察力的但是错过了标记。
答案 2 :(得分:0)
ICollection
知道它包含的项目数(Count)。它不必迭代任何项来确定它。以HashSet
类(实现ICollection
)为例。
IEnumerable<T>
不知道它包含多少项。您必须枚举整个列表以确定项目数(计数)。
在LINQ语句中包装ICollection
并不会提高效率。无论你怎么扭转转弯,都必须列举ICollection。