C# 为什么Enumerable.Single()会迭代所有元素,即使已经找到了多个项?
在分析我们的一个应用程序时,我们发现在一些代码中出现了一个神秘的减速,我们正在调用C# 为什么Enumerable.Single()会迭代所有元素,即使已经找到了多个项?,c#,linq,.net-4.0,C#,Linq,.net 4.0,在分析我们的一个应用程序时,我们发现在一些代码中出现了一个神秘的减速,我们正在调用Enumerable.Single(source,predicate),用于一个大型集合,该集合有多个项在集合开始处与谓词匹配 调查显示,情况如下: public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { TS
Enumerable.Single(source,predicate)
,用于一个大型集合,该集合有多个项在集合开始处与谓词匹配
调查显示,情况如下:
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();
}
在这种情况下,他们努力为
IList
添加优化,您似乎不是唯一一个这样想的人。具有优化的版本:
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;
}
}
}
使用(IEnumerator e=source.GetEnumerator())
{
while(如MoveNext())
{
t源结果=e.电流;
if(谓词(结果))
{
while(如MoveNext())
{
if(谓语(e.Current))
{
抛出错误。超过一个匹配();
}
}
返回结果;
}
}
}
所以要回答你的问题:除了开发人员没有考虑优化这个用例之外,似乎没有什么“好”的理由
现在的代码是:
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();
}
publicstatictsourcesingle(此IEnumerable源,Func谓词)
{
if(source==null)
{
抛出错误。ArgumentNull(nameof(source));
}
if(谓词==null)
{
抛出错误.ArgumentNull(nameof(谓词));
}
使用(IEnumerator e=source.GetEnumerator())
{
while(如MoveNext())
{
t源结果=e.电流;
if(谓词(结果))
{
while(如MoveNext())
{
if(谓语(e.Current))
{
抛出错误。超过一个匹配();
}
}
返回结果;
}
}
}
抛出错误。NoMatch();
}
只要可能,代码甚至会检查目标是否为IList
,以避免迭代:
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();
}
公共静态TSource Single(此IEnumerable源)
{
if(source==null)
{
抛出错误。ArgumentNull(nameof(source));
}
if(来源为IList列表)
{
开关(list.Count)
{
案例0:
抛出错误。NoElements();
案例1:
返回列表[0];
}
}
其他的
{
使用(IEnumerator e=source.GetEnumerator())
{
如果(!e.MoveNext())
{
抛出错误。NoElements();
}
t源结果=e.电流;
如果(!e.MoveNext())
{
返回结果;
}
}
}
抛出错误。超过一个元素();
}
更新
检查结果表明,迭代优化早在2016年就应用了
IList
优化是1年前添加的,可能是作为核心2.1优化的一部分正如其他答案所指出的,优化已经应用,但是我想提出一个假设,他们是这样做的,最初考虑的是他们没有办法保证谓词函数没有副作用
我不确定这样的行为是否真的会被使用/有用,但这是一个需要考虑的问题。优化故障路径很少值得去做。@Damien_不相信这种情况,他们为什么要优化
SingleOrDefault(此IEnumerable源代码)
将源强制转换为IList并直接检查计数?如果在第二次匹配后枚举序列时引发异常,会发生什么情况?早点回来会改变这种行为。然而,这是一个值得怀疑的结果。有趣的是,对于Where
和Single
(在完整的.NET框架上)相比Single
和predicate,性能实际上更好。Nice find@matthewatson。由于某些原因,2015/2016年的复印机不容易找到。。。我们只有2011年和2013年。有人需要决定关闭哪种方式-可能是旧的dup,因为它有更新的答案。这回答了问题:这实际上是一个错失的机会,这是正确的。@MatthewWatson在2.1推出时添加了很多优化,但Single.cs似乎要追溯到2016年。不幸的是,这不仅仅是“开发人员不考虑优化”,这是一个(摊销)一个简单算法的渐进式低效率实现。这是一个简单的性能缺陷……而且是一个非常糟糕的缺陷。我同意你可以这样分类。这就是为什么社区在优化.NET Core的性能方面投入了如此多的工作。正是这些小的更改对@KonradRudolph的性能产生了巨大的影响看起来程序行为的一部分是在存在多个匹配项时抛出错误。如果不遍历整个过程,它将无法满足所述行为collection@danielmhanover那不是真的。代码检查“多个”.Two不止一个,因此当您有第二个匹配项时可以停止。@IMil它与向后兼容性无关,因为语义是相同的,只有性能特征会发生变化(急剧变化)。您对引发异常的理解也是错误的:是的,应该避免该路径,但具有讽刺意味的是,这是一条快速路径。慢速路径不会引发异常(但需要检查每个元素)。公平地说,预期性能始终为O(n)从技术上来说,问题是“为什么它会迭代所有元素”,其他问题回答了所有国家的监督,因为我给出了一个替代答案,而不是声称设计师不称职。我明白你的意思了,我为没有这样做道歉
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();
}
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();
}