Фон:
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
. См. в другом месте на SO, почему эти свойства 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.
ОБНОВЛЕНИЕ после комментария пользователя supercat
о переполнении стека в другом месте.
Конечно, Linq был представлен в .NET 3.5 (2008 г.), а IReadOnlyCollection<>
— только в .NET 4.5 (2012 г.). Однако в промежутке между ними в .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<>
. - (новое) Также полезна проверка на
IReadOnlyCollection<TSource>
, даже если типsource
является одновременноICollection<>
иIReadOnlyCollection<>
, если применяется общая ковариация. В частности,IEnumerable<TSource>
на самом деле может бытьICollection<SomeSpecializedSourceClass>
, гдеSomeSpecializedSourceClass
преобразуется путем преобразования ссылки вTSource
.ICollection<>
не является ковариантным. Однако проверка наIReadOnlyCollection<TSource>
будет работать по ковариации; любойIReadOnlyCollection<SomeSpecializedSourceClass>
также являетсяIReadOnlyCollection<TSource>
, и будет использоваться ярлык. - Стоимость составляет одну дополнительную проверку типа во время выполнения за вызов метода Linq
Count
.
IReadOnlyCollection<>
(также)? - person Jeppe Stig Nielsen   schedule 08.04.2014ICollection
в устаревших настраиваемых списках только для того, чтобы воспользоваться преимуществами оптимизации LINQ в этой области. - person Adam Houldsworth   schedule 08.04.2014List<Cat>
вводится во время компиляции какIEnumerable<Animal>
(поэтомуAnimal
является базовым классомCat
), расширениеCount
не найдет ярлык. В этом поможет проверка ковариантного интерфейса, такого какIReadOnlyCollection<out T>
. Вопрос обновлен. - person Jeppe Stig Nielsen   schedule 30.04.2014IReadOnlyCollection<T>
, но не реализуют неуниверсальныйICollection
? Я бы подумал, что последнее лучше искать, хотя, возможно, следовало бы создать неуниверсальныйICountable
со свойствомCount
и наследовать от него все остальные типы коллекций. Я думаю, что реализации свойстваCount
других типов коллекций будут рассматриваться как реализацииICountable.Count
, поэтому изменение могло быть совместимо с существующим кодом. - person supercat   schedule 30.04.2014Collection
— единственный независимый от типа интерфейс, предоставляющий эту возможность, которую могут реализовать многие коллекции. Было бы лучше, если бы был необщийICountable
со свойствомCount
, и чтобыIReadableCollection<T>
наследовалось от него иIEnumerable<T>
, но MS поступила иначе. - person supercat   schedule 30.04.2014