C# 中的数组如何部分实现 IList<T>?

Posted

技术标签:

【中文标题】C# 中的数组如何部分实现 IList<T>?【英文标题】:How do arrays in C# partially implement IList<T>? 【发布时间】:2012-06-25 04:07:03 【问题描述】:

您可能知道,C# 中的数组实现了IList&lt;T&gt; 以及其他接口。但不知何故,他们这样做并没有公开实现 IList&lt;T&gt; 的 Count 属性!数组只有一个 Length 属性。

这是 C#/.NET 打破其自己的接口实现规则的明目张胆的例子,还是我遗漏了什么?

【问题讨论】:

没有人说Array 类必须用 C# 编写! Array 是一个“魔法”类,无法在 C# 或任何其他面向 .net 的语言中实现。但是这个特定的特性在 C# 中是可用的。 【参考方案1】:

Explicit interface implementation。简而言之,您可以将其声明为 void IControl.Paint() int IList&lt;T&gt;.Count get return 0;

【讨论】:

【参考方案2】:

根据汉斯回答的新答案

感谢 Hans 给出的答案,我们可以看到实现比我们想象的要复杂一些。编译器和 CLR 都尝试非常努力给人一种数组类型实现 IList&lt;T&gt; 的印象 - 但数组差异使这变得更加棘手。与 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&lt;T&gt;。 C# 规范的第 12.1.2 节是这样说的。因此,无论底层实现做什么,该语言都必须表现,就好像T[] 的类型实现IList&lt;T&gt; 与任何其他接口一样。从这个角度来看,接口 实现的,其中一些成员被显式实现(例如Count)。这是在 语言 级别上对正在发生的事情的最佳解释。

请注意,这仅适用于一维数组(以及从零开始的数组,而不是 C# 作为一种语言对非从零开始的数组有任何说明)。 T[,] 没有实现IList&lt;T&gt;

从 CLR 的角度来看,正在发生一些更时髦的事情。您无法获取通用接口类型的接口映射。例如:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

给出以下例外:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

那么为什么会出现这种奇怪现象呢?好吧,我相信这真的是由于数组协方差,这是类型系统中的一个缺陷,IMO。即使IList&lt;T&gt;协变的(并且不能安全),数组协方差允许它工作:

string[] strings =  "a", "b", "c" ;
IList<object> objects = strings;

...这使得它看起来typeof(string[]) 实现 IList&lt;object&gt;,但实际上并非如此。

CLI 规范 (ECMA-335) 分区 1,第 8.7.1 节有:

签名类型 T 与签名类型 U 兼容当且仅当以下至少一项成立时

...

T 是从零开始的 rank-1 数组 V[]UIList&lt;W&gt;,V 是与 W 兼容的数组元素。

(它实际上并没有提到ICollection&lt;W&gt;IEnumerable&lt;W&gt;,我认为这是规范中的一个错误。)

对于不变性,CLI 规范直接与语言规范一起使用。来自分区 1 的第 8.9.1 节:

此外,一个元素类型为 T 的已创建向量实现了接口 System.Collections.Generic.IList&lt;U&gt;,其中 U := T。(第 8.7 节)

向量是一个零基数的一维数组。)

现在就实现细节而言,显然 CLR 正在做一些时髦的映射以保持此处的分配兼容性:当要求 string[] 实现 ICollection&lt;object&gt;.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&lt;T&gt;.Count 实现explicitly:

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

这样做是为了当您有一个简单的数组变量时,您不会同时拥有 CountLength 直接可用。

通常,当您希望确保可以以特定方式使用类型时,会使用显式接口实现,而不是强制该类型的所有使用者都以这种方式考虑它。

编辑:糟糕,记错了。 ICollection.Count 是显式实现的。通用的IList&lt;T&gt; 被处理为Hans descibes below。

【讨论】:

让我想知道,为什么他们不只是调用属性 Count 而不是 Length? Array 是唯一具有此类属性的常见集合(除非您算上 string)。 @TimS 一个好问题(我不知道他的答案。)我推测原因是因为“计数”意味着一些项目,而数组具有不可更改的“长度” " 一旦被分配(不管哪些元素有值)。 @TimS 我认为这已经完成,因为ICollection 声明了Count,如果其中包含“collection”一词的类型没有不要使用Count :)。在做出这些决定时总是需要权衡取舍。 @JohnSaunders:再说一次......只是对 no 有用信息投了反对票。 @JohnSaunders:我仍然不相信。 Hans 提到了 SSCLI 实现,但也声称数组类型甚至没有实现 IList&lt;T&gt;,尽管语言和 CLI 规范似乎都相反。我敢说接口实现在幕后工作的方式可能很复杂,但在许多情况下都是如此。你还会否决某人说System.String 是不可变的,仅仅因为internal 工作是可变的?出于所有实际目的——当然就C#语言而言——它显式实现。【参考方案4】:

它与 IList 的显式接口实现没有什么不同。仅仅因为您实现了接口并不意味着它的成员需要作为类成员出现。它确实实现了 Count 属性,只是没有在 X[] 上公开它。

【讨论】:

【参考方案5】:

如您所知,C# 中的数组实现了IList&lt;T&gt; 以及其他接口

嗯,是的,呃不,不是。这是 .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&lt;T&gt;。无论如何,非泛型类型当然可以实现泛型接口......这很有效,因为有很多 不同的 类型 - typeof(int[]) != typeof(string[]), so typeof(int [])` 实现了IList&lt;int&gt;typeof(string[]) 实现了IList&lt;string&gt; @HansPassant:请不要以为我会因为某件事令人不安而投反对票。事实仍然是,您通过Array 进行推理(正如您所展示的,它是一个抽象类,因此不可能是数组对象的 actual 类型)和结论(它没有't implement IList&lt;T&gt;) 是不正确的 IMO。我同意它实现IList&lt;T&gt;方式 是不寻常且有趣的——但这纯粹是一个实现 细节。声称T[] 没有实现IList&lt;T&gt; 是误导IMO。它违反了规范和所有观察到的行为。 好吧,你肯定认为这是不正确的。你不能让它与你在规格中读到的东西混为一谈。请随意以您的方式查看它,但您永远无法很好地解释 GetInterfaceMap() 失败的原因。 “一些时髦的东西”并不是什么深刻的见解。我戴着实现眼镜:当然它失败了,它是像鸭子一样的嘎嘎打字,具体的数组类型实际上并没有实现 ICollection。没有什么时髦的。让我们把它留在这里,我们永远不会同意。 至少删除声称数组无法实现 IList&lt;T&gt; 因为 Array 没有的虚假逻辑怎么样?这种逻辑是我不同意的很大一部分。除此之外,我认为我们必须就实现接口的类型的定义达成一致:在我看来,数组类型显示实现@987654339 的类型的所有可观察特征 @,除了GetInterfaceMapping。同样,如何实现这一点对我来说并不重要,就像我可以说System.String 是不可变的一样,即使实现细节不同。 C++CLI 编译器怎么样?那个明显说“我不知道该怎么做!”并发出错误。它需要显式转换为 IList&lt;T&gt; 才能工作。【参考方案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>?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中返回 IList<T> vs Array?

C# IList<T>的用法?

c#中LIST的应用实例谁给个加注释的!灰常感谢!

哪个标准范围适用于C#8中的数组和字符串?

List

如何转换通用IList IList?