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 не может учитывать их всех.