Производительность Skip (и подобных функций, таких как Take)

Я просто посмотрел на исходный код методов расширения Skip/Take.NET Framework (по типу IEnumerable<T>) и обнаружил, что внутренняя реализация работает с методом GetEnumerator

// .NET framework
    public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> source, int count)  
    {
        if (source == null) throw Error.ArgumentNull("source"); 
        return SkipIterator<TSource>(source, count); 
    }

    static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count) 
    {
        using (IEnumerator<TSource> e = source.GetEnumerator()) 
        {
            while (count > 0 && e.MoveNext()) count--;
            if (count <= 0) 
            { 
                while (e.MoveNext()) yield return e.Current;
            } 
        } 
    }

Предположим, что у меня есть IEnumerable<T> с 1000 элементами (базовый тип List<T>). Что произойдет, если я делаю list.Skip(990).Take(10)? Будет ли он повторять первые 990 элементов перед тем, как занять последние десять? (так я понимаю). Если да, то я не понимаю, почему Microsoft не реализовала метод Skip следующим образом:

    // Not tested... just to show the idea
    public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, int count)
    {
        if (source is IList<T>)
        {
            IList<T> list = (IList<T>)source;
            for (int i = count; i < list.Count; i++)
            {
                yield return list[i];
            }
        }
        else if (source is IList)
        {
            IList list = (IList)source;
            for (int i = count; i < list.Count; i++)
            {
                yield return (T)list[i];
            }
        }
        else
        {
            // .NET framework
            using (IEnumerator<T> e = source.GetEnumerator())
            {
                while (count > 0 && e.MoveNext()) count--;
                if (count <= 0)
                {
                    while (e.MoveNext()) yield return e.Current;
                }
            }
        }
    }

Фактически, они сделали это для метода Count, например...

    // .NET Framework...
    public static int Count<TSource>(this IEnumerable<TSource> source) 
    {
        if (source == null) throw Error.ArgumentNull("source");

        ICollection<TSource> collectionoft = source as ICollection<TSource>; 
        if (collectionoft != null) return collectionoft.Count;

        ICollection collection = source as ICollection; 
        if (collection != null) return collection.Count; 

        int count = 0;
        using (IEnumerator<TSource> e = source.GetEnumerator())
        { 
            checked 
            {
                while (e.MoveNext()) count++;
            }
        } 
        return count;
    } 

Так какая причина?

Ответ 1

В отличном учебном пособии Джона Скита Linq он обсуждает (кратко) тот самый вопрос:

Хотя большинство из этих операций не могут быть разумно оптимизированы, это было бы целесообразно оптимизировать Skip, когда источник реализует IList. Мы можем пропустить пропуск, так сказать, и перейти прямо к соответствующий индекс. Это не означает, что источник измененный между итерациями, что может быть одной из причин, по которым оно не как я понимаю.

Похоже, что это разумная причина, чтобы отложить эту оптимизацию, но я согласен, что для конкретных случаев может оказаться целесообразным сделать эту оптимизацию, если вы можете гарантировать, что ваш источник не может/не будет изменен.

Ответ 2

Как упоминалось выше, когда Jon Skeet переопределил LINQ, он упомянул, что оптимизация, подобная вашей Skip, не будет определять случай, когда источник был изменен между итерациями ". Вы можете изменить свой код на следующее, чтобы проверить его. Он делает это, вызывая MoveNext() в перечислителе коллекции, даже если он не использует e.Current, так что метод будет бросать if коллекция меняется.

Конечно, это устраняет значительную часть оптимизации: необходимо, чтобы перечислитель должен был быть создан, частично пройден и удален, но он по-прежнему имеет то преимущество, что вам не нужно бесцельно пройти первый count объекты. И это может сбивать с толку, что у вас есть e.Current, что не полезно, поскольку оно указывает на list[i - count] вместо list[i].

public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, int count)
{
    using (IEnumerator<T> e = source.GetEnumerator())
    {
        if (source is IList<T>)
        {
            IList<T> list = (IList<T>)source;
            for (int i = count; i < list.Count; i++)
            {
                e.MoveNext();
                yield return list[i];
            }
        }
        else if (source is IList)
        {
            IList list = (IList)source;
            for (int i = count; i < list.Count; i++)
            {
                e.MoveNext();
                yield return (T)list[i];
            }
        }
        else
        {
            // .NET framework
            while (count > 0 && e.MoveNext()) count--;
            if (count <= 0)
            {
                while (e.MoveNext()) yield return e.Current;
            }
        }
    }
}

Ответ 3

Я предполагаю, что они хотели бросить InvalidOperationException "Коллекция была изменена...", когда базовая коллекция изменяется тем временем в другом потоке. Ваша версия этого не делает. Это принесет ужасные результаты.

Это стандартная практика. MSFT выполняет всю среду .NET во всех коллекциях, которые не являются потокобезопасными (некоторые из них являются исключительными).