为什么 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' 原因,除了开发人员没有考虑优化这个用例。

优化was applied in .NET Core

现在的密码是:

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,但请牢记这一点。