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


99

如您所知,C#中的数组实现IList<T>了其他接口。尽管不知何故,他们没有公开实现Count的Count属性就这样做了IList<T>。数组只有Length属性。

这是C#/。NET违反其关于接口实现的规则的公然示例还是我遗漏了一些东西?


2
没有人说Array必须使用C#编写该类!
user541686

Array是一个“魔术”类,无法使用C#或任何其他针对.net的语言来实现。但是此特定功能在C#中可用。
CodesInChaos 2012年

Answers:


81

根据汉斯的答案的新答案

感谢汉斯给出的答案,我们可以看到实现比我们想象的要复杂一些。编译器和CLR都非常努力地给人以数组类型实现的印象IList<T>-但是数组差异使这一点变得棘手。与Hans的答案相反,数组类型(无论 单维还是从零开始)确实可以直接实现泛型集合,因为任何特定数组的类型都不System.Array -这只是数组的基本类型。如果您问一个数组类型它支持什么接口,它包括通用类型:

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[],并且UIList<W>,并且V是与W兼容的数组元素。

(实际上并没有提到 ICollection<W>IEnumerable<W>我认为是规范中的错误。)

对于非差异性,CLI规范与语言规范直接匹配。从分区1的8.9.1节开始:

此外,创建的元素类型为T的向量将实现interface System.Collections.Generic.IList<U>,其中U:=T。(第8.7节)

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

现在,在条款实施细则,明确了CLR做一些时髦的映射,以保持分配兼容性这里:当string[]被要求执行ICollection<object>.Count,它不能处理,在相当正常的方式。这算作显式接口实现吗?我认为以这种方式进行处理是合理的,因为除非您直接要求接口映射,否则从语言角度来看,它始终会以这种方式运行。

ICollection.Count

到目前为止,我已经讨论了泛型接口,但是还有非泛型ICollection及其Count属性。这次我们可以获取接口映射,并且实际上该接口是直接由实现的System.ArrayICollection.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
    }
}

5
我认为您将在foo.M1()上遇到编译时失败;不是foo.M2();
Kevin Aenmey 2012年

这里的挑战是要有一个非通用类(如数组)实现一个通用接口类型(如IList <>)。您的代码段不这样做。
汉斯·帕桑

@HansPassant:使非泛型类实现泛型接口类型非常容易。不重要的。我没有看到任何迹象表明那是OP所要问的。
乔恩·斯基特

4
@JohnSaunders:实际上,我不相信之前有任何不正确的地方。我已经对其进行了很多扩展,并解释了为什么CLR奇怪地对待数组-但是我相信显式接口实现的答案以前是非常正确的。您以哪种方式不同意?同样,细节将是有用的(可能的话,您可以自己回答)。
乔恩·斯基特

1
@RBT:是的,尽管使用Count罚款是有区别的,但是Add因为数组是固定大小的,所以总是会抛出错误。
乔恩·斯基特

86

如您所知,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 <>。不能,数组不是通用的。通用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(aka Metro)编写托管代码的语言投影中发生的情况相比,CLR中的这种替代相当温和。几乎所有核心​​.NET类型都在那里被替换。例如,IList <>映射到IVector <>,这是一个完全不受管的类型。COM本身是替代品,不支持泛型类型。

好吧,那是看幕后发生的事情。这可能是非常不舒服,陌生和陌生的海洋,并且地图的尽头有龙。使地球变得平坦并为托管代码中实际发生的事情制作不同的图像非常有用。将它映射到每个人都喜欢的答案很舒服。对于值类型,这不太好用(不要变异一个结构!),但是这个隐藏得很好。GetInterfaceMap()方法失败是我能想到的唯一的泄漏。


1
那是Array该类的声明,而不是数组的类型。它是数组的基本类型。C#中的一维数组确实实现了IList<T>。非泛型类型肯定可以实现泛型接口...之所以起作用,是因为存在许多不同的类型- typeof(int[])!= typeof(string []), so typeof(int [])`实现IList<int>typeof(string[])Implements IList<string>
乔恩·斯基特

2
@HansPassant:请不要仅仅因为它令人不安而认为我会拒绝投票。事实仍然是,您的推理方法Array(如您所显示的是一个抽象类,因此不可能是数组对象的实际类型)和结论(未实现IList<T>)都是不正确的IMO。该方法在其实现IList<T>是不寻常的,有趣的,我会同意-但是这纯粹是一种实现细节。声称T[]未实现IList<T>会误导IMO。它违反规范和所有观察到的行为。
乔恩·斯基特

6
好吧,请确保您认为这是不正确的。您无法使其与规格书中的内容相得益彰。请随意查看,但是您永远不会想出一个很好的解释为什么GetInterfaceMap()失败。“有些时髦”并不是什么见识。我戴着实现眼镜:当然,它失败了,它像鸭子一样打字,具体的数组类型实际上并没有实现ICollection <>。没什么好时髦的。让我们保留在这里,我们永远不会同意。
汉斯·帕桑

4
怎么样,至少去除伪逻辑索赔阵列无法实现IList<T> ,因为 Array不?这种逻辑是我不同意的很大一部分。除此之外,我想我们不得不对意味着什么类型实现一个接口定义达成一致:在我看来,数组类型显示所有观察到的功能的它们实现的类型IList<T>,比其他GetInterfaceMapping。再次说明,实现方式对我而言并不那么重要,就像我很好地说这System.String是不可变的,即使实现细节不同。
乔恩·斯基特

1
C ++ CLI编译器呢?那个人显然说:“我不知道该怎么做!” 并发出错误。它需要显式转换IList<T>才能起作用。
Tobias Knauss

21

IList<T>.Count明确实现:

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

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

通常,当您要确保可以以特定方式使用某种类型而不必强迫该类型的所有使用者以这种方式考虑时,将使用显式接口实现。

编辑:哎呀,糟糕的回忆在那里。ICollection.Count是明确实现的。泛型IList<T>以下Hans的描述处理。


4
但是,令我感到奇怪的是,为什么他们不只是调用属性Count而不是Length呢?Array是唯一具有这种属性的常见集合(除非您计数string)。
Tim S.

5
@TimS一个很好的问题(我不知道该问题的答案。)我推测原因是因为“计数”意味着某些项目,而数组一旦分配就具有不变的“长度”(不论哪个元素具有值。)
dlev 2012年

1
@TimS我认为这样做是因为ICollection声明了Count,如果其中不带单词“ collection”的类型不使用:) ,那将更加令人困惑Count。做出这些决定时总是要权衡取舍。
dlev 2012年

4
@JohnSaunders:再说一次……只是毫无意义的选票。
乔恩·斯基特

5
@JohnSaunders:我仍然不相信。Hans提到了SSCLI实现,但还声称IList<T>尽管语言和CLI规范似乎相反,但数组类型甚至没有实现。我敢说接口实现的工作方式可能很复杂,但是在许多情况下就是这种情况。您是否还会System.String因为内部运作易变而拒绝有人说这是不可变的?出于所有实际目的(当然就C#语言而言),这明确的隐含含义。
乔恩·斯基特


2

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


1

提供参考源:

//----------------------------------------------------------------------------------------
// ! 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,找到相应的通用方法(仅 通过方法名称进行匹配,实例化其类型并执行它。

(强调我的)

来源(向上滚动)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.