В каких случаях 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 нужно будет перечислить.