104

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

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

4

6 回答 6

90

如您所知,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() 方法失败是我能想到的抽象中唯一的泄漏。

于 2012-06-22T21:13:25.507 回答
84

根据汉斯的答案的新答案

感谢 Hans 给出的答案,我们可以看到实现比我们想象的要复杂一些。编译器和 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[])implements 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 的向量实现了接口System.Collections.Generic.IList<U>,其中 U := T。(第 8.7 节)

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

现在就实现细节而言,显然 CLR 正在做一些时髦的映射以保持此处的赋值兼容性:当string[]要求 a 实现 时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
    }
}
于 2012-06-22T20:04:31.720 回答
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描述。

于 2012-06-22T20:04:31.923 回答
10

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

于 2012-06-22T20:04:26.433 回答
2

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

于 2012-06-22T20:05:32.133 回答
2

有可用的参考资源:

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

(强调我的)

来源(向上滚动)。

于 2015-06-05T15:59:41.870 回答