Linq Enumerable.Count проверяет методы ICollection <>, но не для IReadOnlyCollection <>
Фон:
Linq-To-Objects имеет расширение метод Count()
(перегрузка не принимает предикат). Конечно, иногда, когда для метода требуется только IEnumerable<out T>
(для выполнения Linq), мы действительно передадим ему "более богатый" объект, например ICollection<T>
. В этой ситуации было бы расточительно фактически перебирать всю коллекцию (т.е. Получить перечислитель и "перемещаться дальше" целую кучу раз), чтобы определить счетчик, поскольку существует свойство ICollection<T>.Count
для этой цели. И этот "ярлык" использовался в BCL с начала Linq.
Теперь, начиная с .NET 4.5 (2012), есть еще один очень приятный интерфейс, а именно IReadOnlyCollection<out T>
. Это похоже на ICollection<T>
, за исключением того, что он включает только те элементы, которые возвращают T
. По этой причине он может быть ковариантным в T
( "out T
" ), точно так же, как IEnumerable<out T>
, и это действительно хорошо, когда типы элементов могут быть более или менее производными. Но новый интерфейс имеет свое собственное свойство, IReadOnlyCollection<out T>.Count
. См. В другом месте о том, почему эти свойства Count
отличаются (а не только одним свойством).
Вопрос:
Метод Linq Enumerable.Count(this source)
проверяет наличие ICollection<T>.Count
, но не проверяет IReadOnlyCollection<out T>.Count
.
Учитывая, что использование Linq в коллекциях только для чтения является естественным и обычным, было бы хорошей идеей изменить BCL для проверки обоих интерфейсов? Думаю, для этого потребуется еще один тип проверки.
И будет ли это изменением (учитывая, что они не "запомнили" это сделать из версии 4.5, где был введен новый интерфейс)?
Пример кода
Запустите код:
var x = new MyColl();
if (x.Count() == 1000000000)
{
}
var y = new MyOtherColl();
if (y.Count() == 1000000000)
{
}
где MyColl
- это реализация типа IReadOnlyCollection<>
, но не ICollection<>
, а MyOtherColl
- это реализация типа ICollection<>
. В частности, я использовал простые/минимальные классы:
class MyColl : IReadOnlyCollection<Guid>
{
public int Count
{
get
{
Console.WriteLine("MyColl.Count called");
// Just for testing, implementation irrelevant:
return 0;
}
}
public IEnumerator<Guid> GetEnumerator()
{
Console.WriteLine("MyColl.GetEnumerator called");
// Just for testing, implementation irrelevant:
return ((IReadOnlyCollection<Guid>)(new Guid[] { })).GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
Console.WriteLine("MyColl.System.Collections.IEnumerable.GetEnumerator called");
return GetEnumerator();
}
}
class MyOtherColl : ICollection<Guid>
{
public int Count
{
get
{
Console.WriteLine("MyOtherColl.Count called");
// Just for testing, implementation irrelevant:
return 0;
}
}
public bool IsReadOnly
{
get
{
return true;
}
}
public IEnumerator<Guid> GetEnumerator()
{
Console.WriteLine("MyOtherColl.GetEnumerator called");
// Just for testing, implementation irrelevant:
return ((IReadOnlyCollection<Guid>)(new Guid[] { })).GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
Console.WriteLine("MyOtherColl.System.Collections.IEnumerable.GetEnumerator called");
return GetEnumerator();
}
public bool Contains(Guid item) { throw new NotImplementedException(); }
public void CopyTo(Guid[] array, int arrayIndex) { throw new NotImplementedException(); }
public bool Remove(Guid item) { throw new NotSupportedException(); }
public void Add(Guid item) { throw new NotSupportedException(); }
public void Clear() { throw new NotSupportedException(); }
}
и получил результат:
MyColl.GetEnumerator called
MyOtherColl.Count called
из прогона кода, который показывает, что "ярлык" не использовался в первом случае (IReadOnlyCollection<out T>
). Тот же результат показан в 4.5 и 4.5.1.
UPDATE после комментария в другом месте о переполнении стека пользователем supercat
.
Linq был введен в .NET 3.5 (2008), конечно, и IReadOnlyCollection<>
был представлен только в .NET 4.5 (2012). Однако между ними появилась еще одна особенность, ковариация в generics, в .NET 4.0 (2010). Как я сказал выше, IEnumerable<out T>
стал ковариантным интерфейсом. Но ICollection<T>
оставался инвариантным в T
(так как он содержит члены типа void Add(T item);
).
Уже в 2010 году (.NET 4) это привело к тому, что если метод расширения Linq Count
использовался для источника времени компиляции IEnumerable<Animal>
, где фактический тип времени выполнения был, например, List<Cat>
, скажем, это, несомненно, IEnumerable<Cat>
, но также, по ковариации, IEnumerable<Animal>
, тогда "ярлык" не использовался. Метод расширения Count
проверяет, только если тип времени выполнения ICollection<Animal>
, который не является (ковариантность). Он не может проверить ICollection<Cat>
(как он знает, что такое Cat
, его параметр TSource
равен Animal
?).
Позвольте мне привести пример:
static void ProcessAnimals(IEnuemrable<Animal> animals)
{
int count = animals.Count(); // Linq extension Enumerable.Count<Animal>(animals)
// ...
}
то
List<Animal> li1 = GetSome_HUGE_ListOfAnimals();
ProcessAnimals(li1); // fine, will use shortcut to ICollection<Animal>.Count property
List<Cat> li2 = GetSome_HUGE_ListOfCats();
ProcessAnimals(li2); // works, but inoptimal, will iterate through entire List<> to find count
Моя предложенная проверка для IReadOnlyCollection<out T>
также "исправила" эту проблему, так как это один ковариантный интерфейс, который реализуется List<T>
.
Вывод:
- Также проверка на
IReadOnlyCollection<TSource>
была бы полезна в тех случаях, когда тип выполнения source
реализует IReadOnlyCollection<>
, но не ICollection<>
, потому что основной класс коллекции настаивает на том, что он является типом коллекции только для чтения и, следовательно, желает не выполнять ICollection<>
.
- (new) Также проверка на
IReadOnlyCollection<TSource>
выгодна, даже если тип source
равен ICollection<>
и IReadOnlyCollection<>
, если применяется общая ковариация. В частности, IEnumerable<TSource>
может действительно быть ICollection<SomeSpecializedSourceClass>
, где SomeSpecializedSourceClass
конвертируется путем преобразования ссылок в TSource
. ICollection<>
не ковариантно. Однако проверка IReadOnlyCollection<TSource>
будет выполняться ковариацией; любой IReadOnlyCollection<SomeSpecializedSourceClass>
также является IReadOnlyCollection<TSource>
, и ярлык будет использоваться.
- Стоимость - это одна дополнительная проверка типа времени выполнения для вызова метода Linq
Count
.
Ответы
Ответ 1
Во многих случаях класс, реализующий IReadOnlyCollection<T>
, также реализует ICollection<T>
. Таким образом, вы все равно получите выгоду от ярлыка свойства Count.
См. ReadOnlyCollection, например.
public class ReadOnlyCollection<T> : IList<T>,
ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>,
IEnumerable<T>, IEnumerable
Поскольку его плохая практика проверять наличие других интерфейсов для доступа за пределы данного интерфейса только для чтения, это должно быть так.
Реализация дополнительной проверки типа для IReadOnlyInterface<T>
в Count()
будет дополнительным балластом для каждого вызова объекта, который не реализует IReadOnlyInterface<T>
.
Ответ 2
Основываясь на документации MSDN, ICollection<T>
- единственный тип, который получает это специальное обращение:
Если тип источника реализует ICollection <T> , эта реализация используется для получения количества элементов. В противном случае этот метод определяет счетчик.
Я предполагаю, что они не понимали, что для этой оптимизации стоит столкнуться с кодовой базой LINQ (и ее спецификацией). Существует много типов CLR, у которых есть собственное свойство Count
, но LINQ не может учитывать их всех.