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>,

Заключение:

  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'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 не может объяснить все из них.

Другие вопросы по тегам