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

107

你可能知道,C#中的数组实现了 IList<T>等其他接口。但是,他们没有公开实现 IList<T> 的Count属性,而只有 Length 属性。

这是C#/.NET违反其自己的接口实现规则的一个明显例子吗?还是我漏掉了什么?


2
没有人说 Array 类必须用 C# 编写。 - user541686
“数组”是一种“神奇”的类,无法在C#或任何其他针对.NET的语言中实现。但是这个特定的功能在C#中是可用的。 - CodesInChaos
6个回答

94

正如您所知,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;
    }

是的,你当然可以称呼那条评论为“违反规则”的 :) 它本来就非常方便。而且它被极好地隐藏起来了,你可以在CLR的共享源代码分发SSCLI20中查看。搜索“IList”即可查看类型替换发生的位置。最好的实践地点是clr/src/vm/array.cpp中的GetActualImplementationForArrayGenericIListMethod()方法。
与允许为WinRT(又名Metro)编写托管代码的CLR中的语言投影相比,CLR中的这种替换相当温和。几乎任何核心的.NET类型在那里都会被替换。例如,IList<>映射到IVector<>,一个完全未管理的类型。它本身就是一种替换,COM不支持泛型类型。
那么,这就是幕后所发生的事情。它可能是非常不舒服、奇怪和陌生的海洋,有着生活在地图末端的龙。将地球变平并模拟出不同于托管代码真正发生的情景可能非常有用。以此方式将其映射到每个人喜欢的答案是非常舒适的。虽然对于值类型不太适用(不要改变结构体!),但这种方法被隐藏得非常好。我能想到的唯一泄漏这种抽象的方法是GetInterfaceMap()方法失败。

1
这是Array类的声明,它不是数组的类型,而是数组的基本类型。在C#中,单维数组确实实现了IList<T>。非泛型类型当然也可以实现泛型接口...这是因为有许多不同的类型——typeof(int[]) != typeof(string[]),所以typeof(int[])实现了IList<int>,而typeof(string[])实现了IList<string> - Jon Skeet
2
@HansPassant:请不要假设我会因为某些事情令人感到不安而对其进行投票。事实仍然是,你通过Array的推理(正如你展示的那样,它是一个抽象类,所以不可能是数组对象的实际类型)和结论(它不实现IList<T>)在我看来是不正确的。它实现IList<T>方式是不寻常和有趣的,我同意这一点 - 但这纯粹是一个实现细节。声称T[]不实现IList<T>是误导性的,在我看来。这违反了规范和所有观察到的行为。 - Jon Skeet
6
当然,你认为它不正确。你无法使它与规格书中的内容相一致。请随意按照你的方式去看待它,但你永远无法提出一个合理的解释来解释为什么GetInterfaceMap()会失败。"Something funky"并不是一个很好的见解。我带着实现眼镜: 当然它失败了,这就像鸭子一样的类型检查,在具体的数组类型中实际上没有实现ICollection<>接口。这没什么奇怪的。让我们把它留在这里,我们永远不会达成一致。 - Hans Passant
4
至少可以移除那些错误的逻辑,它声称数组不能实现IList<T>是因为Array没有实现,这个逻辑是我不同意的主要部分。除此之外,我认为我们需要就一个类型实现一个接口的定义达成共识:在我的理解中,数组类型展示了除了GetInterfaceMapping以外的实现了IList<T>的所有可观测特征。再次强调,对我来说,如何实现这一点并不那么重要,就像我说System.String是不可变的,尽管具体的实现细节是不同的。 - Jon Skeet
1
C++CLI编译器怎么办?显然它会说:“我不知道该怎么做!”并且会发出一个错误。它需要一个明确的转换到IList<T>才能正常工作。 - Tobias Knauss
显示剩余5条评论

85

根据Hans的回答更新

感谢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[] 类型,即使其实际上是显式实现某些成员(如 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.

那么为什么会出现这种奇怪的情况呢?我认为这实际上是由于数组协变性导致的,而在我看来,数组协变性是类型系统中的一个缺陷。尽管 IList 不是协变的(也不能安全地成为协变的),但是数组协变性允许它起作用:
string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

...这使得看起来typeof(string[])实现了IList<object>,但实际上并不是这样。

CLI规范(ECMA-335)第1部分第8.7.1节中有这样一段:

如果以下至少一个条件成立,则签名类型T与签名类型U兼容:

...

T是一个从零开始的一维数组V [],U是IList ,V与W的数组元素兼容。 (实际上它没有提到ICollection 或IEnumerable ,我认为这是规范中的错误。)对于非变性,CLI规范直接遵循语言规范。来自第1部分8.9.1节:此外,使用元素类型T创建的向量实现接口System.Collections.Generic.IList ,其中U:= T。(§8.7)(向量是具有零基础的单维数组。)
现在就实现细节而言,显然CLR正在进行一些有趣的映射以保持赋值兼容性:当要求string[]作为ICollection<object>.Count的实现时,它无法以完全正常的方式处理。这算不算显式接口实现?我认为将其视为这种方式是合理的,因为从语言角度来看,除非直接要求接口映射,否则它总是以这种方式表现。
那么ICollection.Count呢?
到目前为止,我已经谈论了通用接口,但是还有带有Count属性的非通用ICollection。这次我们可以获得接口映射,事实上,System.Array直接实现了该接口。在Array中,ICollection.Count属性实现的文档说明它是通过显式接口实现来实现的。
如果有人能想到这种显式接口实现与“正常”的显式接口实现不同的方式,我很乐意进一步研究。 关于显式接口实现的旧回答 尽管上述内容由于数组知识而更加复杂,但您仍然可以通过显式接口实现实现相同可见效果。
以下是一个简单的独立示例:
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
4
@JohnSaunders:实际上,我认为之前的回答都是准确的。我已经进行了大量扩展,并解释了CLR为什么会奇怪地处理数组 - 但我相信在这方面我的显式接口实现的答案以前是相当正确的。您对哪一点持不同意见?再次强调,详细信息将是有用的(如果适合的话,可以在您自己的答案中提供)。 - Jon Skeet
当应用于派生类型的数组时,返回的列表不会是详尽无遗的,因为SiameseCat()将实现IList<Animal>,即使后者类型既不在支持的接口列表中,也不被它们所隐含(尽管IList<SiameseCat>隐含了IEnumerable<Animal>,但IList<SiameseCat>并不隐含IList<Animal>)。 - supercat
嘿,Jon!老实说,我也无法理解你或Hans的回答。这似乎是一个非常复杂的话题。但从一个外行人的角度来看,是否可以这样说 - 数组只是显式实现接口,如IListICollection。这不仅涉及到OP提到的Count属性,还包括IList接口中存在的其他协定,例如AddRemove等。在代码中,我总是可以像这样做 - int[] myArr = new int[20];((IList)myArr).Add(4); var myCount = ((ICollection)myArr).Count; - RBT
1
@RBT:是的,虽然有一个区别,使用Count是可以的,但使用Add总是会抛出异常,因为数组的大小是固定的。 - Jon Skeet
显示剩余2条评论

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 呢?数组是唯一一个有这种属性的常见集合(除了字符串)。 - Tim S.
5
@TimS 一个好问题(我不知道答案)。我猜测原因是“count”暗示了一些项目的数量,而数组在分配后就有一个不可改变的“长度”(无论哪些元素具有值)。 - dlev
1
@TimS 我认为这是因为 ICollection 声明了 Count,如果一个带有“collection”一词的类型不使用 Count,那么这将会更加令人困惑 :). 在做出这些决策时总是存在权衡。 - dlev
4
再次来了……只是一个没有任何有用信息的负投票。 - Jon Skeet
5
@JohnSaunders: 我仍然不太确定。Hans提到了SSCLI的实现,但他也声称,尽管语言和CLI规范显示相反,数组类型甚至没有实现IList<T>接口。我敢说,在底层实现接口的方式可能很复杂,但在许多情况下都是如此。如果有人说System.String是不可变的,只是因为内部工作方式是可变的,你是否也会对其进行反对票?对于所有实际目的来说 - 而且肯定就C#语言而言 - 它是显式实现。 - Jon Skeet
显示剩余6条评论

10

显式接口实现。简而言之,您可以像这样声明它:void IControl.Paint() { }int IList<T>.Count { get { return 0; } }


3
有参考来源可用:
//----------------------------------------------------------------------------------------
// ! 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,找到相应的通用方法(仅通过方法名称匹配),为类型实例化它并执行它。

(我强调了一些内容)

来源(向上滚动)。


2

这与 IList 的显式接口实现没有区别。仅仅因为你实现了接口,并不意味着它的成员必须出现在类成员中。 它确实实现了 Count 属性,只是没有在 X[] 上公开它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接