Метод 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. См. в другом месте на 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>.

Заключение:

  1. Также проверка IReadOnlyCollection<TSource> будет полезной в тех случаях, когда тип времени выполнения source реализует IReadOnlyCollection<>, но не ICollection<>, потому что базовый класс коллекции настаивает на том, чтобы быть типом коллекции только для чтения и, следовательно, желает не реализовать ICollection<> .
  2. (новое) Также полезна проверка на IReadOnlyCollection<TSource>, даже если тип source является одновременно ICollection<> и IReadOnlyCollection<>, если применяется общая ковариация. В частности, IEnumerable<TSource> на самом деле может быть ICollection<SomeSpecializedSourceClass>, где SomeSpecializedSourceClass преобразуется путем преобразования ссылки в TSource. ICollection<> не является ковариантным. Однако проверка на IReadOnlyCollection<TSource> будет работать по ковариации; любой IReadOnlyCollection<SomeSpecializedSourceClass> также является IReadOnlyCollection<TSource>, и будет использоваться ярлык.
  3. Стоимость составляет одну дополнительную проверку типа во время выполнения за вызов метода Linq Count.

person Jeppe Stig Nielsen    schedule 08.04.2014    source источник
comment
Похоже, в официальной функции Count() просто отсутствует проверка IReadOnlyCollection. Наверное забыли реализовать? Но его оптимизация все-таки.   -  person BlueM    schedule 08.04.2014
comment
@BlueM Итак, мой вопрос был: не было бы хорошей идеей проверить IReadOnlyCollection<> (также)?   -  person Jeppe Stig Nielsen    schedule 08.04.2014
comment
Я имею в виду, что вам приходится иметь дело с одной дополнительной проверкой типа для каждого вызова Count() в IEnumerable. Это может быть неразумно, если IReadOnlyInterface встречается реже.   -  person BlueM    schedule 08.04.2014
comment
@JeppeStigNielsen Обсуждение заключается в том, есть ли какая-либо выгода по сравнению со стоимостью проверки распространенных случаев. Оптимизация почти всегда является компромиссом. В этом случае он тратит дополнительное время на проверку и приведение типов, чтобы избежать повторения. Я бы предположил, что распространенным сценарием является количество обычных списков/коллекций, намного превышающее количество копий только для чтения. В некоторых случаях я реализовал ICollection в устаревших настраиваемых списках только для того, чтобы воспользоваться преимуществами оптимизации LINQ в этой области.   -  person Adam Houldsworth    schedule 08.04.2014
comment
Я уверен, что об этом уже спрашивали...   -  person leppie    schedule 08.04.2014
comment
@AdamHouldsworth Верно, но сегодня я узнал, что это также имеет преимущества из-за ковариации в дженериках. Например, когда List<Cat> вводится во время компиляции как IEnumerable<Animal> (поэтому Animal является базовым классом Cat), расширение Count не найдет ярлык. В этом поможет проверка ковариантного интерфейса, такого как IReadOnlyCollection<out T>. Вопрос обновлен.   -  person Jeppe Stig Nielsen    schedule 30.04.2014
comment
@JeppeStigNielsen: Сколько классов реализуют IReadOnlyCollection<T>, но не реализуют неуниверсальный ICollection? Я бы подумал, что последнее лучше искать, хотя, возможно, следовало бы создать неуниверсальный ICountable со свойством Count и наследовать от него все остальные типы коллекций. Я думаю, что реализации свойства Count других типов коллекций будут рассматриваться как реализации ICountable.Count, поэтому изменение могло быть совместимо с существующим кодом.   -  person supercat    schedule 30.04.2014
comment
@supercat Я предполагаю, что определяемые пользователем классы часто реализуют только соответствующий общий интерфейс. Кто мешает создавать множество явных реализаций интерфейса, чтобы угодить более или менее устаревшему неуниверсальному интерфейсу, который все равно не будет использоваться?   -  person Jeppe Stig Nielsen    schedule 30.04.2014
comment
@JeppeStigNielsen: определение количества элементов в коллекции должно быть операцией, не зависящей от типа, а неуниверсальный Collection — единственный независимый от типа интерфейс, предоставляющий эту возможность, которую могут реализовать многие коллекции. Было бы лучше, если бы был необщий ICountable со свойством Count, и чтобы IReadableCollection<T> наследовалось от него и IEnumerable<T>, но MS поступила иначе.   -  person supercat    schedule 30.04.2014


Ответы (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>.

person BlueM    schedule 08.04.2014
comment
Это верно. При реализации ICollection‹T› вы можете объявить, что коллекция на самом деле доступна только для чтения, таким образом вы можете предоставить доступ к коллекции только для чтения в устаревшем .net. - person ; 30.04.2014
comment
Сегодня мне стало известно о другом преимуществе, а именно, если приведенное выше ReadOnlyCollection<Cat> типизировано как IEnumerable<Animal> по ковариации (Animal является базовым классом Cat), то проверка ICollection<Animal> на самом деле вернет false. Но из-за ковариации проверка IReadOnlyCollection<Animal> вернет истину, поскольку по ковариации IReadOnlyCollection<Cat> является IReadOnlyCollection<Animal>. Смотрите обновленный вопрос (вопрос теперь слишком длинный ...). - person Jeppe Stig Nielsen; 30.04.2014

На основе документации MSDN. , ICollection<T> — единственный тип, который получает эту специальную обработку:

Если тип источника реализует ICollection‹T>, эта реализация используется для получения количества элементов. В противном случае этот метод определяет количество.

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

person JLRishe    schedule 08.04.2014