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

MgS*_*Sam 98 .net c# arrays interface list

您可能知道,C#中的数组实现IList<T>了其他接口.但不知何故,他们这样做没有公开实现Count属性IList<T>!数组只有一个Length属性.

这是一个明显的例子,C#/ .NET打破了自己关于接口实现的规则,还是我错过了什么?

Han*_*ant 84

您可能知道,C#中的数组实现IList<T>了其他接口

嗯,是的,呃不,不是真的.这是.NET 4框架中Array类的声明:

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}
Run Code Online (Sandbox Code Playgroud)

它实现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(); }
}
Run Code Online (Sandbox Code Playgroud)

对于具有"未找到接口"的具体数组类型,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;
    }
Run Code Online (Sandbox Code Playgroud)

是的,你当然可以称这条评论为"违反规则":)否则它会变得很方便.非常隐蔽,您可以在SSRI20(CLR的共享源代码分发)中查看.搜索"IList"以查看类型替换发生的位置.查看它的最佳位置是clr/src/vm/array.cpp,GetActualImplementationForArrayGenericIListMethod()方法.

与CLR中允许为WinRT(又称Metro)编写托管代码的语言投影相比,CLR中的这种替换相当温和.几乎任何核心的.NET类型都可以替代它.IList <>映射到IVector <>,例如,完全不受管理的类型.它本身是一个替代,COM不支持泛型类型.

嗯,那是看幕后幕后发生的事情.它可能是非常不舒服,奇怪和陌生的海洋,龙生活在地图的尽头.使地球变平并模拟托管代码中真实情况的不同图像非常有用.将它映射到每个人最喜欢的答案都很舒服.哪个值对于值类型不能很好地工作(不要改变结构!)但是这个很好地隐藏了.GetInterfaceMap()方法失败是我能想到的抽象中唯一的漏洞.

  • 好吧,你肯定认为这是不正确的.你不能用你在规格中读到的内容来表达它.请随意看到你的方式,但你永远不会得到一个很好的解释为什么GetInterfaceMap()失败."时髦的东西"并不是很有见地.我戴着实施眼镜:当然它失败了,它是类似于鸭子的庸医打字,具体的数组类型实际上并没有实现ICollection <>.没什么好玩的.让我们把它放在这里,我们永远不会同意. (6认同)
  • 至少删除声称数组无法实现`IList <T>`*的虚假逻辑,因为*`Array`不会?这种逻辑是我不同意的一大部分.除此之外,我认为我们必须就一个类型实现接口的含义的定义达成一致:在我看来,数组类型显示*实现`IList <T>`的类型的所有可观察特征*,除了`GetInterfaceMapping`.再一次,如何实现这一点对我来说不那么重要,就像我说'System.String`是不可变的一样好,即使*实现细节*不同. (4认同)
  • @HansPassant:请不要以为我会因为某件事*令人不安*而拒绝投票。事实仍然是,您通过 `Array` 进行的推理(如您所示,它是一个抽象类,因此不可能是数组对象的 *actual* 类型)和结论(它没有实现 `IList &lt;T&gt;`) 是不正确的 IMO。它实现`IList&lt;T&gt;`的*方式*是不寻常和有趣的,我同意 - 但这纯粹是*实现*细节。声称`T[]` 没有实现`IList&lt;T&gt;` 是误导IMO。它违反了规范和所有观察到的行为。 (2认同)

Jon*_*eet 80

根据汉斯的回答新答案

感谢Hans给出的答案,我们可以看到实施比我们想象的要复杂一些.编译器和CLR都非常努力地给出数组类型实现的印象IList<T>- 但是数组方差使得这更加棘手.与Hans的答案相反,数组类型(单维,零基础)确实直接实现了泛型集合,因为任何特定数组的类型都不 System.Array - 这只是数组的基本类型.如果你问一个数组类型它支持哪些接口,它包括泛型类型:

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}
Run Code Online (Sandbox Code Playgroud)

输出:

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]
Run Code Online (Sandbox Code Playgroud)

对于单维,从零开始的数组,就语言而言,数组确实也实现IList<T>了.C#规范的第12.1.2节说明了这一点.因此,无论底层实现如何,语言都必须像任何其他接口一样表现T[]实现的类型IList<T>.从这个角度来看,接口通过明确实现的一些成员实现的(例如Count).对于正在发生的事情,这是语言层面的最佳解释.

请注意,这仅适用于一维数组(和基于零的数组,而不是C#作为一种语言说明非基于零的数组).T[,] 没有实现IList<T>.

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

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))
Run Code Online (Sandbox Code Playgroud)

给出例外:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.
Run Code Online (Sandbox Code Playgroud)

那怪小的原因呢?嗯,我相信这真的是由于阵列协方差,这是类型系统中的疣,IMO.即使IList<T>协变(和不能安全),数组协方差让这个工作:

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;
Run Code Online (Sandbox Code Playgroud)

...它使它看起来typeof(string[])工具IList<object>,当它不是真的.

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

当且仅当下列中的至少一个成立时,签名类型T与签名类型U兼容

...

T是基于零的秩1阵列V[],和UIList<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,它不能处理,在相当正常的方式.这是否算作显式接口实现?我认为这是合理的方式来对待它,除非你直接问的接口映射,它总是表现从语言的角度的方式.

怎么样ICollection.Count

到目前为止,我已经讨论了泛型接口,但是ICollection它的Count属性是非泛型的.这次我们可以得到接口映射,实际上接口是直接实现的System.Array.ICollection.Count属性实现的文档,Array表明它是通过显式接口实现实现的.

如果有人能想到这种显式接口实现与"普通"显式接口实现不同的方式,我很乐意进一步研究它.

围绕显式接口实现的旧答案

尽管如上所述,由于对数组的了解,这更复杂,但您仍然可以通过显式接口实现来执行具有相同可见效果的操作.

这是一个简单的独立示例:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我想你会在foo.M1()上遇到编译时失败; 不是foo.M2(); (5认同)
  • @JohnSaunders:实际上,我不相信以前任何一个都不准确.我已经扩展了很多,并解释了为什么CLR奇怪地处理数组 - 但我相信我之前对显式接口实现的回答非常正确.你以什么方式不同意?同样,细节将是有用的(如果合适,可能在您自己的答案中). (4认同)

dle*_*lev 20

IList<T>.Count明确实现的:

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);
Run Code Online (Sandbox Code Playgroud)

这样做是为了当你有一个简单的数组变量时,你没有这两个Count并且Length直接可用.

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

编辑:哎呀,不好回忆那里.ICollection.Count明确实施.该通用IList<T>名称由Hans descibes处理.

  • @TimS一个很好的问题(以及我不知道的答案.)我推测原因是因为"count"意味着一定数量的项目,而一个数组在分配后就具有不可改变的"长度"(无论哪个元素都有值.) (5认同)
  • @JohnSaunders:我还是不相信.Hans提到了SSCLI实现,但也声称数组类型甚至没有实现`IList <T>`,尽管语言和CLI规范都出现了相反的情况.我敢说接口实现在封面下工作的方式可能很复杂,但在许多情况下都是这种情况.你是否也会贬低某人说'System.String`是不可变的,只是因为*内部*工作是可变的?对于*所有*实际目的 - 当然就C#语言而言 - 它*是*明确的impl. (5认同)
  • 让我想知道,为什么他们不只是将财产称为Count而不是Length?Array是唯一具有此类属性的常见集合(除非您计算`string`). (4认同)
  • @JohnSaunders:再次......只是一个*没有*有用信息的downvote. (4认同)

Tim*_* S. 10

显式接口实现.简而言之,您将其声明为void IControl.Paint() { }int IList<T>.Count { get { return 0; } }.