C# 中的数组如何部分实现 IList<T>?
Posted
技术标签:
【中文标题】C# 中的数组如何部分实现 IList<T>?【英文标题】:How do arrays in C# partially implement IList<T>? 【发布时间】:2012-06-25 04:07:03 【问题描述】:您可能知道,C# 中的数组实现了IList<T>
以及其他接口。但不知何故,他们这样做并没有公开实现 IList<T>
的 Count 属性!数组只有一个 Length 属性。
这是 C#/.NET 打破其自己的接口实现规则的明目张胆的例子,还是我遗漏了什么?
【问题讨论】:
没有人说Array
类必须用 C# 编写!
Array
是一个“魔法”类,无法在 C# 或任何其他面向 .net 的语言中实现。但是这个特定的特性在 C# 中是可用的。
【参考方案1】:
Explicit interface implementation。简而言之,您可以将其声明为 void IControl.Paint()
或 int IList<T>.Count get return 0;
。
【讨论】:
【参考方案2】:根据汉斯回答的新答案
感谢 Hans 给出的答案,我们可以看到实现比我们想象的要复杂一些。编译器和 CLR 都尝试非常努力给人一种数组类型实现 IList<T>
的印象 - 但数组差异使这变得更加棘手。与 Hans 的答案相反,数组类型(一维,无论如何都是从零开始的)确实直接实现了泛型集合,因为任何特定数组的类型 不是 System.Array
- 这只是数组的 base 类型。如果你问一个数组类型它支持什么接口,它包括泛型类型:
foreach (var type in typeof(int[]).GetInterfaces())
Console.WriteLine(type);
输出:
System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]
对于从零开始的一维数组,就语言而言,该数组确实也实现了IList<T>
。 C# 规范的第 12.1.2 节是这样说的。因此,无论底层实现做什么,该语言都必须表现,就好像T[]
的类型实现IList<T>
与任何其他接口一样。从这个角度来看,接口是 实现的,其中一些成员被显式实现(例如Count
)。这是在 语言 级别上对正在发生的事情的最佳解释。
请注意,这仅适用于一维数组(以及从零开始的数组,而不是 C# 作为一种语言对非从零开始的数组有任何说明)。 T[,]
没有实现IList<T>
。
从 CLR 的角度来看,正在发生一些更时髦的事情。您无法获取通用接口类型的接口映射。例如:
typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))
给出以下例外:
Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.
那么为什么会出现这种奇怪现象呢?好吧,我相信这真的是由于数组协方差,这是类型系统中的一个缺陷,IMO。即使IList<T>
是非协变的(并且不能安全),数组协方差允许它工作:
string[] strings = "a", "b", "c" ;
IList<object> objects = strings;
...这使得它看起来像 typeof(string[])
实现 IList<object>
,但实际上并非如此。
CLI 规范 (ECMA-335) 分区 1,第 8.7.1 节有:
签名类型 T 与签名类型 U 兼容当且仅当以下至少一项成立时
...
T 是从零开始的 rank-1 数组
V[]
,U
是IList<W>
,V 是与 W 兼容的数组元素。
(它实际上并没有提到ICollection<W>
或IEnumerable<W>
,我认为这是规范中的一个错误。)
对于不变性,CLI 规范直接与语言规范一起使用。来自分区 1 的第 8.9.1 节:
此外,一个元素类型为 T 的已创建向量实现了接口
System.Collections.Generic.IList<U>
,其中 U := T。(第 8.7 节)
(向量是一个零基数的一维数组。)
现在就实现细节而言,显然 CLR 正在做一些时髦的映射以保持此处的分配兼容性:当要求 string[]
实现 ICollection<object>.Count
时,它可以't 以正常方式处理它相当。这算作显式接口实现吗?我认为这样处理它是合理的,因为除非您直接要求接口映射,否则从语言的角度来看,它总是表现。
ICollection.Count
呢?
到目前为止,我已经讨论了泛型接口,但还有非泛型 ICollection
及其 Count
属性。这次我们可以得到接口映射,其实接口是直接由System.Array
实现的。 Array
中的 ICollection.Count
属性实现文档声明它是通过显式接口实现实现的。
如果有人能想到这种显式接口实现与“普通”显式接口实现不同的方式,我很乐意进一步研究。
关于显式接口实现的旧答案
尽管上面因为数组的知识而比较复杂,但是你仍然可以通过explicit interface implementation做同样的可见效果。
这是一个简单的独立示例:
public interface IFoo
void M1();
void M2();
public class Foo : IFoo
// Explicit interface implementation
void IFoo.M1()
// Implicit interface implementation
public void M2()
class Test
static void Main()
Foo foo = new Foo();
foo.M1(); // Compile-time failure
foo.M2(); // Fine
IFoo ifoo = foo;
ifoo.M1(); // Fine
ifoo.M2(); // Fine
【讨论】:
我认为你会在 foo.M1();不是 foo.M2(); 这里的挑战是让一个非泛型类,如数组,实现一个泛型接口类型,如 IList。你的 sn-p 不会那样做。 @HansPassant:让非泛型类实现泛型接口类型非常容易。琐碎的。我没有看到任何迹象表明这就是 OP 所要求的。 @JohnSaunders:实际上,我不相信之前有任何不准确的。我已经对其进行了很多扩展,并解释了为什么 CLR 奇怪地对待数组 - 但我相信我之前对显式接口实现的回答是非常正确的。你在什么方面不同意?同样,详细信息会很有用(可能在您自己的答案中,如果合适的话)。 @RBT: 是的,虽然使用Count
是有区别的 - 但Add
总是会抛出,因为数组是固定大小的。【参考方案3】:
IList<T>.Count
实现explicitly:
int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);
这样做是为了当您有一个简单的数组变量时,您不会同时拥有 Count
和 Length
直接可用。
通常,当您希望确保可以以特定方式使用类型时,会使用显式接口实现,而不是强制该类型的所有使用者都以这种方式考虑它。
编辑:糟糕,记错了。 ICollection.Count
是显式实现的。通用的IList<T>
被处理为Hans descibes below。
【讨论】:
让我想知道,为什么他们不只是调用属性 Count 而不是 Length? Array 是唯一具有此类属性的常见集合(除非您算上string
)。
@TimS 一个好问题(我不知道他的答案。)我推测原因是因为“计数”意味着一些项目,而数组具有不可更改的“长度” " 一旦被分配(不管哪些元素有值)。
@TimS 我认为这已经完成,因为ICollection
声明了Count
,如果其中包含“collection”一词的类型没有不要使用Count
:)。在做出这些决定时总是需要权衡取舍。
@JohnSaunders:再说一次......只是对 no 有用信息投了反对票。
@JohnSaunders:我仍然不相信。 Hans 提到了 SSCLI 实现,但也声称数组类型甚至没有实现 IList<T>
,尽管语言和 CLI 规范似乎都相反。我敢说接口实现在幕后工作的方式可能很复杂,但在许多情况下都是如此。你还会否决某人说System.String
是不可变的,仅仅因为internal 工作是可变的?出于所有实际目的——当然就C#语言而言——它是显式实现。【参考方案4】:
它与 IList 的显式接口实现没有什么不同。仅仅因为您实现了接口并不意味着它的成员需要作为类成员出现。它确实实现了 Count 属性,只是没有在 X[] 上公开它。
【讨论】:
【参考方案5】:如您所知,C# 中的数组实现了
IList<T>
以及其他接口
嗯,是的,呃不,不是。这是 .NET 4 框架中 Array 类的声明:
[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable,
IStructuralComparable, IStructuralEquatable
// etc..
它实现了 System.Collections.IList,不是 System.Collections.Generic.IList。它不能, Array 不是通用的。通用 IEnumerable 和 ICollection 接口也是如此。
但是 CLR 动态创建具体的数组类型,因此它可以在技术上创建一个实现这些接口的类型。然而事实并非如此。试试这个代码,例如:
using System;
using System.Collections.Generic;
class Program
static void Main(string[] args)
var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>)); // Kaboom
abstract class Base
class Derived : Base, IEnumerable<int>
public IEnumerator<int> GetEnumerator() return null;
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() return GetEnumerator();
对于具有“未找到接口”的具体数组类型,GetInterfaceMap() 调用失败。然而,转换为 IEnumerable 没有问题。
这是一种像鸭子一样的嘎嘎打字。正是这种类型的类型产生了一种错觉,即每个值类型都派生自从 Object 派生的 ValueType。编译器和 CLR 都对数组类型有特殊的了解,就像它们对值类型一样。编译器看到您尝试转换为 IList 并说“好的,我知道该怎么做!”。并发出 castclass IL 指令。 CLR 对此没有任何问题,它知道如何提供适用于底层数组对象的 IList 的实现。它具有其他隐藏的 System.SZArrayHelper 类的内置知识,该类是实际实现这些接口的包装器。
它不像每个人都声称的那样明确地做,你问的 Count 属性看起来像这样:
internal int get_Count<T>()
//! Warning: "this" is an array, not an SZArrayHelper. See comments above
//! or you may introduce a security hole!
T[] _this = JitHelpers.UnsafeCast<T[]>(this);
return _this.Length;
是的,您当然可以称该评论为“违反规则”:) 否则它非常方便。而且隐藏得非常好,您可以在 SSCLI20(CLR 的共享源代码分发版)中查看这一点。搜索“IList”以查看类型替换发生的位置。最好的查看它的地方是 clr/src/vm/array.cpp,GetActualImplementationForArrayGenericIListMethod() 方法。
与 CLR 中允许为 WinRT(又名 Metro)编写托管代码的语言投影中发生的情况相比,CLR 中的这种替换相当温和。几乎所有核心 .NET 类型都可以在那里被替换。 IList 映射到 IVector 例如,一个完全非托管的类型。 COM 本身是一个替代品,不支持泛型类型。
嗯,那是看看幕后发生的事情。生活在地图尽头的龙可能是非常不舒服、陌生和不熟悉的海洋。使地球变平并为托管代码中实际情况的不同图像建模可能非常有用。将它映射到每个人最喜欢的答案是很舒服的。这对于值类型来说效果不佳(不要改变结构!)但是这个隐藏得很好。 GetInterfaceMap() 方法失败是我能想到的抽象中唯一的泄漏。
【讨论】:
这是Array
类的声明,它不是数组的类型。它是数组的 base 类型。 C# 中的一维数组确实 实现了IList<T>
。无论如何,非泛型类型当然可以实现泛型接口......这很有效,因为有很多 不同的 类型 - typeof(int[])
!= typeof(string[]), so
typeof(int [])` 实现了IList<int>
,typeof(string[])
实现了IList<string>
。
@HansPassant:请不要以为我会因为某件事令人不安而投反对票。事实仍然是,您通过Array
进行推理(正如您所展示的,它是一个抽象类,因此不可能是数组对象的 actual 类型)和结论(它没有't implement IList<T>
) 是不正确的 IMO。我同意它实现IList<T>
的方式 是不寻常且有趣的——但这纯粹是一个实现 细节。声称T[]
没有实现IList<T>
是误导IMO。它违反了规范和所有观察到的行为。
好吧,你肯定认为这是不正确的。你不能让它与你在规格中读到的东西混为一谈。请随意以您的方式查看它,但您永远无法很好地解释 GetInterfaceMap() 失败的原因。 “一些时髦的东西”并不是什么深刻的见解。我戴着实现眼镜:当然它失败了,它是像鸭子一样的嘎嘎打字,具体的数组类型实际上并没有实现 ICollection。没有什么时髦的。让我们把它留在这里,我们永远不会同意。
至少删除声称数组无法实现 IList<T>
因为 Array
没有的虚假逻辑怎么样?这种逻辑是我不同意的很大一部分。除此之外,我认为我们必须就实现接口的类型的定义达成一致:在我看来,数组类型显示实现@987654339 的类型的所有可观察特征 @,除了GetInterfaceMapping
。同样,如何实现这一点对我来说并不重要,就像我可以说System.String
是不可变的一样,即使实现细节不同。
C++CLI 编译器怎么样?那个明显说“我不知道该怎么做!”并发出错误。它需要显式转换为 IList<T>
才能工作。【参考方案6】:
参考源可用:
//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
//
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]".
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
// ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it.
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper
// It is never legal to instantiate this class.
private SZArrayHelper()
Contract.Assert(false, "Hey! How'd I get here?");
/* ... snip ... */
具体来说这部分:
接口存根调度程序将此视为特殊情况,加载 SZArrayHelper,找到对应的泛型方法(简单匹配 按方法名),将其实例化为类型并执行。
(强调我的)
Source(向上滚动)。
【讨论】:
以上是关于C# 中的数组如何部分实现 IList<T>?的主要内容,如果未能解决你的问题,请参考以下文章