C# 中的数组是如何实现泛型集合接口

我们先来看一段C#代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
var array = new string[] { "abc", "xyz" };
Foo(array);

var list = new List<string> { "abc", "xyz" };
Foo(list);
}

static void Foo(IList<string> list)
{
Console.WriteLine($"list count: {list.Count}");
foreach (var item in list)
{
Console.WriteLine(item);
}
}

在Visual Studio中编译、执行,一切正常。你会觉得这段代码很普通,没啥特别之处。但仔细观察,你会发现string数组竟然可以隐式地转换成IList<T>这个接口,这不是很奇怪吗?

打开.NET Reference Source,我们可以看到List<string>之所以能传入,是因为List实现了IList<T>接口:
List<T>

Array并没有实现IList<T>接口:
List<T>

那么,Array类型是如何获得泛型IList<T>的接口实现呢?

这一切都是编译器和CLR的功劳

首先Array是一个抽象类,我们在代码中使用方括号[]来声明一个数组时,编译器会为我们创建一个具体的数组类,这个和泛型的机制很类似。这个类自然也就实现了ICloneable, IList, IStructuralComparable, IStructuralEquatable这几个非泛型的接口。但这并不能解释它是如何实现泛型接口的。
其次,Array是可以支持多维的,比如下面的代码声明了一个二维的3 * 3的数组:

1
2
3
4
5
6
7
8
9
int[,] matrix = new int[3, 3]
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

Console.WriteLine(matrix.Rank); // 2
Console.WriteLine(matrix.Length); // 9

多维数组其实就是数学中的矩阵,一般三维以上的数组就很少使用了。与此同时你会发现,只有一维数组实现了泛型集合接口:

1
2
3
4
5
6
7
8
9
10
int[,] matrix = new int[3, 3]
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
IList<int> list1 = (IList<int>)matrix; // compile error

var array = new int[] { 1, 2, 3 };
IList<int> list2 = (IList<int>)array;

如果你仔细阅读Array类型的源码,你会发现类声明的上面有如下的注释:

Note that we make a T[] (single-dimensional w/ zero as the lower bound) implement both IList<U> and IReadOnlyList<U>, where T : U dynamically. See the SZArrayHelper class for details.

这里的single-dimensional w/ zero as the lower bound就是一维的、以0为最低下标(single-dimensional, zero-based)的意思。注释中提到的SZArrayHelper中的SZ分别指代Single-dimensional和Zero-based。在Array类的代码中,很快可以找到SZArrayHelper类的代码,看注释就豁然开朗了。
SZArrayHelper

最近刚好在PluralSight上看到一门课程,正好佐证了这个特殊的例子。
PluralSight

The Array class was provided in the first release of the .NET framework before .NET supported generics. Starting with .NET framework 2, the Array class was modified to implement these generic interfaces. But these implementations are provided to arrays at runtime, and as a result, the generic interfaces do not appear in the declaration syntax for the Array class, so we can work with an array through these interfaces, but we cannot access the properties and methods of these interfaces when working with an array directly.

Deborah Kuratapluralsight.com

我们还可以使用代码来验证这一点。打开Visual Studio,找到C# Interactive窗口,编写如下代码:

1
2
3
4
foreach (var type in typeof(int[]).GetInterfaces())
{
Console.WriteLine(type);
}

输出:
C# Interactive

里氏代换原则

另外这个小例子还体现了C#中的面向对象的一个特性——里氏替换原则(Liskov Substitution Principle,LSP)。里氏替换原则是指所有引用基类的地方必须能透明地使用其子类的对象。本例中Foo方法接收一个IList<string>类型的对象作为参数,但传入的参数不仅可以是IList<string>对象,还可以是IList<string>类型的子类——List<string>string[]。使用泛型,你会发现我们可以写出更加通用、简洁的代码。

总结

在编译器和CLR的协同工作下,数组类型在编译时可以转换成IList<T>ICollection<T>IEnumerable<T>等泛型集合接口,以获得这些接口提供的方法和属性;在运行时CLR动态的实现了这些接口以保证正确的调用。

相关链接