Linq 的 Enumerable.Count 方法检查 ICollection<>但不适用于 IReadOnlyCollection<> [英] Linq's Enumerable.Count method checks for ICollection<> but not for IReadOnlyCollection<>
问题描述
背景:
Linq-To-Objects 具有扩展 方法 Count()
(不带谓词的重载).当然有时当一个方法只需要一个IEnumerable
(做Linq)时,我们真的会传递一个更丰富"的对象给它,比如一个ICollection
ICollection
用于此目的.而这个捷径"从Linq开始就在BCL中使用.
现在,从 .NET 4.5(2012 年)开始,还有另一个非常好的界面,即 IReadOnlyCollection
.它类似于 ICollection
,只是它只包括那些返回一个 T
的成员.出于这个原因,它可以在 T
("out T
") 中协变,就像 IEnumerable
一样,这真的很好当项目类型可以或多或少地派生时.但是新界面有自己的属性,IReadOnlyCollection
.请参阅别处 关于为什么这些 Count
属性不同(而不是只有一个属性).>
问题:
Linq 的方法 Enumerable.Count(this source)
会检查 ICollection
,但不会检查 IReadOnlyCollection
.
鉴于在只读集合上使用 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
变成了协变接口.但是 ICollection
在 T
中保持不变(因为它包含像 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
实现的协变接口.
结论:
- 同时检查
IReadOnlyCollection
在source
的运行时类型实现IReadOnlyCollection<>
但不是ICollection<>
因为底层集合类坚持是只读集合类型,因此希望不实现ICollection<>
. - (new) 检查
IReadOnlyCollection
也是有益的,即使source
的类型是ICollection<>
和IReadOnlyCollection<>
,如果通用协方差适用.具体来说,IEnumerable
可能真的是一个ICollection
,其中SomeSpecializedSourceClass
可以通过引用转换为TSource
代码>.ICollection<>
不是协变的.但是,对IReadOnlyCollection
的检查将通过协方差进行;任何IReadOnlyCollection
也是IReadOnlyCollection
,并且将使用快捷方式. - 成本是每次调用 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:
- Also checking for
IReadOnlyCollection<TSource>
would be beneficial in cases where the run-time type ofsource
implementsIReadOnlyCollection<>
but notICollection<>
because the underlying collection class insists on being a read-only collection type and therefore wishes to not implementICollection<>
. - (new) Also checking for
IReadOnlyCollection<TSource>
is beneficial even when the type ofsource
is bothICollection<>
andIReadOnlyCollection<>
, if generic covariance applies. Specifically, theIEnumerable<TSource>
may really be anICollection<SomeSpecializedSourceClass>
whereSomeSpecializedSourceClass
is convertible by reference conversion toTSource
.ICollection<>
is not covariant. However, the check forIReadOnlyCollection<TSource>
will work by covariance; anyIReadOnlyCollection<SomeSpecializedSourceClass>
is also anIReadOnlyCollection<TSource>
, and the shortcut will be utilized. - 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<>但不适用于 IReadOnlyCollection<>的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!