В каких случаях IEnumerable <T>.Count оптимизирован?

Использование reflector Я заметил, что System.Linq.Enumerable.Count имеет в нем условие, чтобы оптимизировать его для случая, когда переданный IEnumerable<T> фактически является ICollection<T>. Если листинг преуспевает, метод Count не требует итерации по каждому элементу, но может вызвать метод Count ICollection.

Исходя из этого, я начал думать, что IEnumerable<T> можно использовать как просмотр в режиме просмотра только для чтения, без потери производительности, которую я изначально ожидал, на основе API IEnumerable<T>

Мне было интересно, сохраняется ли оптимизация Count, когда IEnumerable<T> является результатом оператора Select над ICollection, но на основе отраженного кода этот случай не оптимизирован и требует итерация по всем элементам.

Вы делаете те же выводы из рефлектора? Что может быть причиной отсутствия этой оптимизации? Мне кажется, что в этой общей операции много времени потрачено впустую. Требует ли спецификация, чтобы каждый элемент оценивался, даже если граф можно определить без этого?

Ответы

Ответ 1

На самом деле не имеет значения, что результат Select лениво оценивается. Count всегда эквивалентен счету исходной коллекции, поэтому его можно было бы, конечно, получить непосредственно, возвратив конкретный объект из Select, который можно было бы использовать для оценки короткого замыкания метода Count.

Причина, по которой невозможно оптимизировать оценку метода Count() для возвращаемого значения вызова Select от чего-то с определенным счетчиком (например, <<26 > ), заключается в том, что он может изменить значение программы,

Функция selector, переданная в метод Select, имеет возможность иметь побочные эффекты, и ее побочные эффекты должны выполняться детерминистически в заданном порядке.

Предположим:

new[]{1,2,3}.Select(i => { Console.WriteLine(i); return 0; }).Count();

Документация требует, чтобы этот код печатал

1
2
3

Даже если подсчет действительно известен с самого начала и может быть оптимизирован, оптимизация изменит поведение программы. Вот почему вы не можете избежать перечисления коллекции в любом случае. Это одна из причин, по которой оптимизация компилятора намного проще в чистых функциональных языках.


UPDATE: По-видимому, не ясно, что вполне возможно реализовать Select и Count, чтобы Select on ICollection<T> все равно был лениво оценен, но Count() будет оцениваться в O (1) без перечисления коллекции. Я собираюсь сделать это, не изменяя интерфейс каких-либо методов. Аналогичная вещь уже сделана для ICollection<T>:

private interface IDirectlyCountable {
    int Count {get;}
}
private class SelectICollectionIterator<TSource,TResult> : IEnumerable<T>, IDirectlyCountable {
     ICollection<TSource> sequence;
     Func<TSource,TResult> selector;
     public SelectICollectionIterator(ICollection<TSource> source, Func<TSource,TResult> selector) {
         this.sequence = source;
         this.selector = selector;
     }
     public int Count { get { return sequence.Count; } }
     // ... GetEnumerator ... 
}
public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource,TResult> selector) {
    // ... error handling omitted for brevity ...
    if (source is ICollection<TSource>)
       return new SelectICollectionIterator<TSource,TResult>((ICollection<TSource>)source, selector);
    // ... rest of the method ...
}
public static int Count<T>(this IEnumerable<T> source) {
    // ...
    ICollection<T> collection = source as ICollection<T>;
    if (collection != null) return collection.Count;
    IDirectlyCountable countableSequence = source as IDirectlyCountable;
    if (countableSequence != null) return countableSequence.Count;
    // ... enumerate and count the sequence ...
}

Это все равно будет оценивать Count. Если вы измените базовую коллекцию, счетчик будет изменен, и последовательность не будет кэшироваться. Единственное отличие будет не в том, чтобы делать побочные эффекты в делегате selector.

Ответ 2

Изменить 02-фев-2010:

Как я вижу, существует как минимум два способа интерпретировать этот вопрос.

Почему метод расширения Select<T, TResult>, когда вызов экземпляра класса, который реализует ICollection<T>, а не вернуть объект, который предоставляет Count свойство; и почему Count<T> метод расширения не проверьте это свойство, чтобы обеспечивают производительность O (1), когда два методы связаны цепью?

Эта версия вопроса не делает ложных предположений о том, как работают расширения Linq, и является допустимым вопросом, поскольку вызов ICollection<T>.Select.Count будет, в конце концов, всегда возвращать то же значение, что и ICollection<T>.Count. Так Мехрдад интерпретировал вопрос, на который он дал тщательный ответ.

Но я читал вопрос, спрашивая...

Если метод расширения Count<T> обеспечивает O (1) производительность для объекта класса внедрение ICollection<T>, почему обеспечивает ли она O (n) производительность для возвращаемое значение Select<T, TResult>метод расширения?

В этой версии вопроса есть ошибочное предположение: методы расширения Linq работают вместе, собирая маленькие коллекции один за другим (в памяти) и подвергая их через интерфейс IEnumerable<T>.

Если бы так работали расширения Linq, метод Select мог бы выглядеть примерно так:

public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
    List<TResult> results = new List<TResult>();

    foreach (T input in source)
        results.Add(selector(input));

    return results;
}

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

В действительности, я считаю, что реализация метода Select намного ближе к чему-то вроде этого:

public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
    foreach (T input in source)
        yield return selector(input);

    yield break;
}

Это дает ленивую оценку и объясняет, почему свойство Count недоступно в O (1) времени методу Count.

Иными словами, в то время как Мехрдад ответил на вопрос, почему Select не был разработан по-другому, так что Select.Count будет вести себя по-другому, я предложил свой лучший ответ на вопрос о том, почему Select.Count ведет себя так, как это делает.


ОРИГИНАЛЬНЫЙ ОТВЕТ:

Побочные эффекты метода не являются ответом.

Согласно Мехрдаду, ответьте:

Не имеет значения, что результат Select оценивается лениво.

Я не покупаю это. Позвольте мне объяснить, почему.

Для начала рассмотрим следующие два очень похожих метода:

public static IEnumerable<double> GetRandomsAsEnumerable(int N) {
    Random r = new Random();

    for (int i = 0; i < N; ++i)
        yield return r.NextDouble();

    yield break;
}

public static double[] GetRandomsAsArray(int N) {
    Random r = new Random();

    double[] values = new double[N];
    for (int i = 0; i < N; ++i)
        values[i] = r.NextDouble();

    return values;
}

Хорошо, что делают эти методы? Каждый из них возвращает столько случайных удвоений, сколько пожелает пользователь (до int.MaxValue). Имеет ли значение какой-либо метод лениво оценивается или нет? Чтобы ответить на этот вопрос, давайте взглянем на следующий код:

public static double Invert(double value) {
    return 1.0 / value;
}

public static void Test() {
    int a = GetRandomsAsEnumerable(int.MaxValue).Select(Invert).Count();
    int b = GetRandomsAsArray(int.MaxValue).Select(Invert).Count();
}

Можете ли вы догадаться, что произойдет с этими двумя вызовами метода? Позвольте мне избавить вас от необходимости копировать этот код и проверить его самостоятельно:

Переменная первая, a, будет (после потенциально значительного времени) инициализироваться на int.MaxValue (в настоящее время 2147483647). второй, b, скорее всего, будет прерван OutOfMemoryException.

Поскольку Select и другие методы расширения Linq лениво оцениваются, они позволяют делать то, что вы просто не могли сделать иначе. Вышеприведенный пример довольно тривиальный. Но мой главный вопрос - оспаривать утверждение, что ленивая оценка не важна. Утверждение Мехрдада о том, что свойство Count действительно известно с самого начала и может быть оптимизировано, на самом деле вызывает вопрос. Этот вопрос может показаться простым для метода Select, но Select не особо особенный; он возвращает IEnumerable<T> так же, как и остальные методы расширения Linq, и для этих методов "знать" Count их возвращаемых значений потребует кэширования полных коллекций и, следовательно, запрещает ленивую оценку.

Леновая оценка - это ответ.

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

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

Ответ 3

An ICollection знает количество содержащихся в нем элементов (Count). Он не должен перебирать все элементы для его определения. Возьмем, к примеру, класс HashSet (который реализует ICollection).

An IEnumerable<T> не знает, сколько элементов оно содержит. Вы должны перечислить весь список, чтобы определить количество элементов (количество).

Обтекание ICollection в операторе LINQ не делает его более эффективным. Независимо от того, как вы крутите и поворачиваете, ICollection нужно будет перечислить.