Enumerable.Count в Linq проверяет 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
, Смотрите в другом месте на SO, почему этиCount
свойства различны (вместо одного свойства).
Вопрос:
Метод Линка 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 вызвал MyOtherColl.Count вызывается
из кода запуска, который показывает, что "ярлык" не был использован в первом случае (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) это привело к тому, что если Линк Count
метод расширения был использован для источника типа времени компиляции IEnumerable<Animal>
где фактический тип времени выполнения был, например, List<Cat>
скажем, что, безусловно, IEnumerable<Cat>
но, по ковариации, IEnumerable<Animal>
, то "ярлык" не использовался. Count
метод extension проверяет только, является ли тип времени выполнения 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's.
Count
метод.
2 ответа
Во многих случаях класс, который реализует 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>
,
Основываясь на документации MSDN, ICollection<T>
это единственный тип, который получает эту специальную обработку:
Если тип источника реализует ICollection
, эта реализация используется для получения количества элементов. В противном случае этот метод определяет количество.
Я предполагаю, что они не считали целесообразным связываться с базой кода LINQ (и его спецификацией) ради этой оптимизации. Есть много типов CLR, которые имеют свои собственные Count
собственность, но LINQ не может объяснить все из них.