为什么 Enumerable.Single() 迭代所有元素,即使已经找到了不止一项?
Why does Enumerable.Single() iterate all elements, even when more than one item has already been found?
在分析我们的一个应用程序时,我们发现在某些代码中出现神秘的减速,在这些代码中,我们为一个大型集合调用 Enumerable.Single(source, predicate)
,该集合在集合开头附近有多个与谓词匹配的项目。
调查发现the implementation of Enumerable.Single()
如下:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
// Note how this always iterates through ALL the elements:
foreach (TSource element in source) {
if (predicate(element)) {
result = element;
checked { count++; }
}
}
switch (count) {
case 0: throw Error.NoMatch();
case 1: return result;
}
throw Error.MoreThanOneMatch();
}
该实现将遍历序列中的每个元素,即使不止一个元素已经与谓词匹配。
以下实施似乎会产生相同的结果:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
foreach (TSource element in source) {
if (predicate(element)) {
if (count == 1) // Exit loop immediately if more than one match found.
throw Error.MoreThanOneMatch();
result = element;
count++; // "checked" is no longer needed.
}
}
if (count == 0)
throw Error.NoMatch();
return result;
}
有谁知道为什么实际实现不使用这种明显的优化?有什么我想念的吗? (我无法想象如此明显的优化会被忽视,因此一定有一些具体的原因。)
(注意:我意识到这个问题可能会吸引一些意见的答案;我希望得到的答案能够提供迭代所有元素的具体原因。如果答案实际上是 "because the designers didn't think such an optimization was necessary",那么这个问题就是无法回答,我想我应该删除它...)
为了比较,看一下不带谓词的Single()
的实现:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
IList<TSource> list = source as IList<TSource>;
if (list != null) {
switch (list.Count) {
case 0: throw Error.NoElements();
case 1: return list[0];
}
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (!e.MoveNext()) throw Error.NoElements();
TSource result = e.Current;
if (!e.MoveNext()) return result;
}
}
throw Error.MoreThanOneElement();
}
在这种情况下,他们努力为 IList
添加优化。
你似乎不是唯一一个这么想的人。 .NET Core implementation 有一个优化版本:
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
所以回答你的问题:似乎没有 'good' 原因,除了开发人员没有考虑优化这个用例。
现在的密码是:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (predicate == null)
{
throw Error.ArgumentNull(nameof(predicate));
}
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
throw Error.NoMatch();
}
只要有可能,代码甚至会检查目标是否为 IList<T>
,这样就可以避免迭代:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (source is IList<TSource> list)
{
switch (list.Count)
{
case 0:
throw Error.NoElements();
case 1:
return list[0];
}
}
else
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (!e.MoveNext())
{
throw Error.NoElements();
}
TSource result = e.Current;
if (!e.MoveNext())
{
return result;
}
}
}
throw Error.MoreThanOneElement();
}
更新
检查 git blame 输出显示早在 2016 年就应用了迭代优化!
IList<>
优化是在 1 年前添加的,可能是 Core 2.1 优化的一部分
正如其他答案所指出的那样,已经应用了优化,但我只想提出一个假设,他们最初是这样做的,考虑到他们无法保证谓词的事实函数没有副作用。
我不确定是否真的会出现这种行为 used/useful,但请牢记这一点。
在分析我们的一个应用程序时,我们发现在某些代码中出现神秘的减速,在这些代码中,我们为一个大型集合调用 Enumerable.Single(source, predicate)
,该集合在集合开头附近有多个与谓词匹配的项目。
调查发现the implementation of Enumerable.Single()
如下:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
// Note how this always iterates through ALL the elements:
foreach (TSource element in source) {
if (predicate(element)) {
result = element;
checked { count++; }
}
}
switch (count) {
case 0: throw Error.NoMatch();
case 1: return result;
}
throw Error.MoreThanOneMatch();
}
该实现将遍历序列中的每个元素,即使不止一个元素已经与谓词匹配。
以下实施似乎会产生相同的结果:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
foreach (TSource element in source) {
if (predicate(element)) {
if (count == 1) // Exit loop immediately if more than one match found.
throw Error.MoreThanOneMatch();
result = element;
count++; // "checked" is no longer needed.
}
}
if (count == 0)
throw Error.NoMatch();
return result;
}
有谁知道为什么实际实现不使用这种明显的优化?有什么我想念的吗? (我无法想象如此明显的优化会被忽视,因此一定有一些具体的原因。)
(注意:我意识到这个问题可能会吸引一些意见的答案;我希望得到的答案能够提供迭代所有元素的具体原因。如果答案实际上是 "because the designers didn't think such an optimization was necessary",那么这个问题就是无法回答,我想我应该删除它...)
为了比较,看一下不带谓词的Single()
的实现:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
IList<TSource> list = source as IList<TSource>;
if (list != null) {
switch (list.Count) {
case 0: throw Error.NoElements();
case 1: return list[0];
}
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (!e.MoveNext()) throw Error.NoElements();
TSource result = e.Current;
if (!e.MoveNext()) return result;
}
}
throw Error.MoreThanOneElement();
}
在这种情况下,他们努力为 IList
添加优化。
你似乎不是唯一一个这么想的人。 .NET Core implementation 有一个优化版本:
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
所以回答你的问题:似乎没有 'good' 原因,除了开发人员没有考虑优化这个用例。
现在的密码是:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (predicate == null)
{
throw Error.ArgumentNull(nameof(predicate));
}
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
throw Error.NoMatch();
}
只要有可能,代码甚至会检查目标是否为 IList<T>
,这样就可以避免迭代:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (source is IList<TSource> list)
{
switch (list.Count)
{
case 0:
throw Error.NoElements();
case 1:
return list[0];
}
}
else
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (!e.MoveNext())
{
throw Error.NoElements();
}
TSource result = e.Current;
if (!e.MoveNext())
{
return result;
}
}
}
throw Error.MoreThanOneElement();
}
更新
检查 git blame 输出显示早在 2016 年就应用了迭代优化!
IList<>
优化是在 1 年前添加的,可能是 Core 2.1 优化的一部分
正如其他答案所指出的那样,已经应用了优化,但我只想提出一个假设,他们最初是这样做的,考虑到他们无法保证谓词的事实函数没有副作用。
我不确定是否真的会出现这种行为 used/useful,但请牢记这一点。