Linq 的 Enumerable.Count 方法检查 ICollection<>但不适用于 IReadOnlyCollection<> [英] Linq's Enumerable.Count method checks for ICollection<> but not for IReadOnlyCollection<>

查看:20
本文介绍了Linq 的 Enumerable.Count 方法检查 ICollection<>但不适用于 IReadOnlyCollection<>的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景:

Linq-To-Objects 具有扩展 方法 Count()(不带谓词的重载).当然有时当一个方法只需要一个IEnumerable(做Linq)时,我们真的会传递一个更丰富"的对象给它,比如一个ICollection.在那种情况下,实际上遍历整个集合(即获取枚举器并移动下一个"一大堆)来确定计数是很浪费的,因为有一个 property ICollection.Count 用于此目的.而这个捷径"从Linq开始就在BCL中使用.

现在,从 .NET 4.5(2012 年)开始,还有另一个非常好的界面,即 IReadOnlyCollection.它类似于 ICollection,只是它只包括那些返回一个 T 的成员.出于这个原因,它可以在 T ("out T") 中协变,就像 IEnumerable 一样,这真的很好当项目类型可以或多或少地派生时.但是新界面有自己的属性,IReadOnlyCollection.计数.请参阅别处 关于为什么这些 Count 属性不同(而不是只有一个属性).>

问题:

Linq 的方法 Enumerable.Count(this source) 会检查 ICollection.Count,但不会检查 IReadOnlyCollection.Count.

鉴于在只读集合上使用 Linq 非常自然和常见,更改 BCL 以检查两个接口是否是个好主意?我想这需要额外的一个类型检查.

这是否会是一个重大变化(因为他们没有记得"从引入新界面的 4.5 版本开始这样做)?

示例代码

运行代码:

 var x = new MyColl();如果 (x.Count() == 1000000000){}var y = new MyOtherColl();如果 (y.Count() == 1000000000){}

其中 MyColl 是实现 IReadOnlyCollection<> 而不是 ICollection<> 的类型,并且 MyOtherColl> 是一种实现 ICollection<> 的类型.具体来说,我使用了简单/最小的类:

class MyColl : IReadOnlyCollection{公共整数计数{得到{Console.WriteLine("MyCol.Count 调用");//仅用于测试,与实现无关:返回0;}}公共 IEnumerator获取枚举器(){Console.WriteLine("MyColl.GetEnumerator 被调用");//仅用于测试,与实现无关:return ((IReadOnlyCollection)(new Guid[] { })).GetEnumerator();}System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator(){Console.WriteLine("MyColl.System.Collections.IEnumerable.GetEnumerator 被调用");返回 GetEnumerator();}}MyOtherColl 类:ICollection{公共整数计数{得到{Console.WriteLine("MyOtherColl.Count 调用");//仅用于测试,与实现无关:返回0;}}public bool IsReadOnly{得到{返回真;}}公共 IEnumerator获取枚举器(){Console.WriteLine("MyOtherColl.GetEnumerator 被调用");//仅用于测试,与实现无关:return ((IReadOnlyCollection)(new Guid[] { })).GetEnumerator();}System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator(){Console.WriteLine("MyOtherColl.System.Collections.IEnumerable.GetEnumerator 被调用");返回 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 被调用MyOtherCol.Count 调用

从代码运行来看,这表明在第一种情况下没有使用快捷方式"(IReadOnlyCollection).在 4.5 和 4.5.1 中看到相同的结果.

<小时>

UPDATE 在用户 supercat 在 Stack Overflow 的其他地方发表评论后.

Linq 当然是在 .NET 3.5 (2008) 中引入的,而 IReadOnlyCollection<> 只在 .NET 4.5 (2012) 中引入.然而,在两者之间,在 .NET 4.0 (2010) 中引入了另一个特性,泛型中的协变.正如我上面所说,IEnumerable 变成了协变接口.但是 ICollectionT 中保持不变(因为它包含像 void Add(T item); 这样的成员).

在 2010 年 (.NET 4) 中,如果在编译时类型 IEnumerable 的源上使用 Linq 的 Count 扩展方法,则其结果是例如,实际的运行时类型是 List,比如说,它肯定是一个 IEnumerable,但根据协方差,它也是一个 IEnumerable.Animal>,则没有使用快捷方式".Count 扩展方法仅检查运行时类型是否为 ICollection,而事实并非如此(无协方差).它无法检查 ICollection(它怎么知道 Cat 是什么,它的 TSource 参数等于 Animal?).

举个例子:

static void ProcessAnimals(IEnuemrable动物){int count = Animals.Count();//Linq 扩展 Enumerable.Count(animals)//...}

然后:

Listli1 = GetSome_HUGE_ListOfAnimals();处理动物(li1);//很好,将使用 ICollection.Count 属性的快捷方式列表<猫>li2 = GetSome_HUGE_ListOfCats();处理动物(li2);//有效,但不是最优的,将遍历整个 List<>找到计数

我建议检查 IReadOnlyCollection 也会修复"这个问题,因为这是一个由 List 实现的协变接口.

结论:

  1. 同时检查 IReadOnlyCollectionsource 的运行时类型实现 IReadOnlyCollection<> 但不是 ICollection<> 因为底层集合类坚持是只读集合类型,因此希望实现 ICollection<>.
  2. (new) 检查 IReadOnlyCollection 也是有益的,即使 source 的类型是 ICollection<>IReadOnlyCollection<>,如果通用协方差适用.具体来说,IEnumerable 可能真的是一个 ICollection,其中 SomeSpecializedSourceClass 可以通过引用转换为 TSource代码>.ICollection<> 不是协变的.但是,对 IReadOnlyCollection 的检查将通过协方差进行;任何 IReadOnlyCollection 也是 IReadOnlyCollection,并且将使用快捷方式.
  3. 成本是每次调用 Linq 的 Count 方法时进行一次额外的运行时类型检查.

解决方案

在很多情况下,实现 IReadOnlyCollection 的类也将实现 ICollection.因此,您仍将受益于 Count 属性快捷方式.

例如,参见 ReadOnlyCollection.

公共类 ReadOnlyCollection:IList,ICollection、IList、ICollection、IReadOnlyList、IReadOnlyCollection、IEnumerable, IEnumerable

由于检查其他接口以获取给定只读接口之外的访问权限的不好做法,因此应该可以这样做.

Count() 中的 IReadOnlyInterface 实施额外的类型检查将是对未实现 IReadOnlyInterface< 的对象的每次调用的额外镇流器;T>.

Background:

Linq-To-Objects has the extension method Count() (the overload not taking a predicate). Of course sometimes when a method requires only an IEnumerable<out T> (to do Linq), we will really pass a "richer" object to it, such as an ICollection<T>. In that situation it would be wasteful to actually iterate through the entire collection (i.e. get the enumerator and "move next" a whole bunch of times) to determine the count, for there is a property ICollection<T>.Count for this purpose. And this "shortcut" has been used in the BCL since the beginning of Linq.

Now, since .NET 4.5 (of 2012), there is another very nice interface, namely IReadOnlyCollection<out T>. It is like the ICollection<T> except that it only includes those member that return a T. For that reason it can be covariant in T ("out T"), just like IEnumerable<out T>, and that is really nice when item types can be more or less derived. But the new interface has its own property, IReadOnlyCollection<out T>.Count. See elsewhere on SO why these Count properties are distinct (instead of just one property).

The question:

Linq's method Enumerable.Count(this source) does check for ICollection<T>.Count, but it does not check for IReadOnlyCollection<out T>.Count.

Given that it is really natural and common to use Linq on read-only collections, would it be a good idea to change the BCL to check for both interfaces? I guess it would require one additional type check.

And would that be a breaking change (given that they did not "remember" to do this from the 4.5 version where the new interface was introduced)?

Sample code

Run the code:

    var x = new MyColl();
    if (x.Count() == 1000000000)
    {
    }

    var y = new MyOtherColl();
    if (y.Count() == 1000000000)
    {
    }

where MyColl is a type implementing IReadOnlyCollection<> but not ICollection<>, and where MyOtherColl is a type implementing ICollection<>. Specifically I used the simple/minimal classes:

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(); }
}

and got the output:

MyColl.GetEnumerator called
MyOtherColl.Count called

from the code run, which shows that the "shortcut" was not used in the first case (IReadOnlyCollection<out T>). Same result is seen in 4.5 and 4.5.1.


UPDATE after comment elsewhere on Stack Overflow by user supercat.

Linq was introduced in .NET 3.5 (2008), of course, and the IReadOnlyCollection<> was introduced only in .NET 4.5 (2012). However, in between, another feature, covariance in generics was introduced, in .NET 4.0 (2010). As I said above, IEnumerable<out T> became a covariant interface. But ICollection<T> stayed invariant in T (since it contains members like void Add(T item);).

Already in 2010 (.NET 4) this had the consequence that if Linq's Count extension method was used on a source of compile-time type IEnumerable<Animal> where the actual run-time type was for example List<Cat>, say, which is surely an IEnumerable<Cat> but also, by covariance, an IEnumerable<Animal>, then the "shortcut" was not used. The Count extension method checks only if the run-time type is an ICollection<Animal>, which it is not (no covariance). It can't check for ICollection<Cat> (how would it know what a Cat is, its TSource parameter equals Animal?).

Let me give an example:

static void ProcessAnimals(IEnuemrable<Animal> animals)
{
    int count = animals.Count();  // Linq extension Enumerable.Count<Animal>(animals)
    // ...
}

then:

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

My suggested check for IReadOnlyCollection<out T> would "repair" this issue too, since that is one covariant interface which is implemented by List<T>.

Conclusion:

  1. Also checking for IReadOnlyCollection<TSource> would be beneficial in cases where the run-time type of source implements IReadOnlyCollection<> but not ICollection<> because the underlying collection class insists on being a read-only collection type and therefore wishes to not implement ICollection<>.
  2. (new) Also checking for IReadOnlyCollection<TSource> is beneficial even when the type of source is both ICollection<> and IReadOnlyCollection<>, if generic covariance applies. Specifically, the IEnumerable<TSource> may really be an ICollection<SomeSpecializedSourceClass> where SomeSpecializedSourceClass is convertible by reference conversion to TSource. ICollection<> is not covariant. However, the check for IReadOnlyCollection<TSource> will work by covariance; any IReadOnlyCollection<SomeSpecializedSourceClass> is also an IReadOnlyCollection<TSource>, and the shortcut will be utilized.
  3. The cost is one additional run-time type check per call to Linq's Count method.

解决方案

In many cases a class that implements IReadOnlyCollection<T> will also implement ICollection<T>. So you will still profit from the Count property shortcut.

See ReadOnlyCollection for example.

public class ReadOnlyCollection<T> : IList<T>, 
    ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, 
    IEnumerable<T>, IEnumerable

Since its bad practice to check for other interfaces to get access beyond the given readonly interface it should be ok this way.

Implementing an additional type check for IReadOnlyInterface<T> in Count() will be additional ballast for every call on an object which doesn't implement IReadOnlyInterface<T>.

这篇关于Linq 的 Enumerable.Count 方法检查 ICollection&lt;&gt;但不适用于 IReadOnlyCollection&lt;&gt;的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆