考虑以下枚举器:
var items = (new int[] { 1, 2, 3, 4, 5 }).Select(x =>
{
Console.WriteLine($"inspect {x}");
return x;
});
这将产生元素[1, 2, 3, 4, 5]
,并在消耗时打印它们。
当我在此枚举器上调用Last
方法时,它会触发一条仅访问单个元素的快速路径:
items.Last();
inspect 5
但是当我将回调传递给Last
时,它将从头开始遍历整个列表:
items.Last(x => true);
inspect 1
inspect 2
inspect 3
inspect 4
inspect 5
浏览.NET Core源代码,我发现:
Last(IEnumerable<T>)
forwards to TryGetLast(IEnumerable<T>, out bool)
; TryGetLast(IEnumerable<T>, out bool)
has a fast path for IPartition<T>
; ArraySelectIterator<T>
implements IPartition<T>
起,此快速路径便被触发,一切都很好。另一方面:
Last(IEnumerable<T>, Func<T, bool>)
forwards to TryGetLast(IEnumerable<T>, Func<T, bool>, out bool)
OrderedEnumerator
and IList<T>
, but not ArraySelectIterator<T>
提供了快速的路径。这说明了如何未优化回调案例。但这并不能解释为什么。
从概念上讲,如果至少有一个元素满足谓词(在实践中可能是这样),则向后迭代可以允许较早退出循环。
似乎也不是很难实现:据我所知,它所需要的只是IPartition<T>
上的一个附加方法。
缺乏优化也可能令人惊讶。由于这些重载使用相同的名称,因此可以假定它们也以类似的方式进行了优化。 (至少我就是这么想的。)
鉴于这些优化本案的原因,为什么LINQ的作者选择不这样做?
答案 0 :(得分:1)
Last()
始终可以针对允许在恒定时间(O(1)
)中访问集合中最后一个元素的集合进行优化。对于这些集合,您无需直接迭代所有集合并返回最后一个元素,而可以直接访问最后一个元素。
从概念上讲,如果至少一个元素满足谓词(可能在 实践),然后向后迭代可能会导致退出循环较早。
对于Last(Func<T,bool>)
的一般实现而言,这个假设太过复杂了。通常,您不能假设满足谓词的最后一个元素更接近集合的末尾。该 optimization 对于您的示例(Last(x=>true)
来说效果很好,但是对于每个这样的示例,都有一个相反的示例,其中 optimization 的效果较差({{1} }。