历史
如果数组成为泛型类型,会出现什么问题?
在C# 1.0时代,他们主要从Java中复制了数组的概念。当时还没有泛型,但是创建者认为他们很聪明,复制了Java数组具有的破碎的协变数组语义。这意味着你可以像这样做而不会产生编译时错误(而是运行时错误):
Mammoth[] mammoths = new Mammoth[10];
Animal[] animals = mammoths; // Covariant conversion
animals[1] = new Giraffe(); // Run-time exception
在C#2.0中引入了泛型,但没有协变/逆变的泛型类型。如果将数组作为泛型,则不能将强制转换为,尽管以前可以这样做(即使它是错误的)。因此,使数组成为泛型会破坏大量代码。
只有在C#4.0中引入了接口的协变/逆变泛型类型。这使得最终可以修复损坏的数组协变性。但同样,这将破坏许多现有的代码。
Array<Mammoth> mammoths = new Array<Mammoth>(10);
Array<Animal> animals = mammoths; // Not allowed.
IEnumerable<Animals> animals = mammoths; // Covariant conversion
数组实现通用接口
为什么数组不实现通用的 IList<T>
、ICollection<T>
和 IEnumerable<T>
接口呢?
由于运行时技巧,每个数组 T[]
都会自动实现 IEnumerable<T>
、ICollection<T>
和 IList<T>
。1 根据 Array
类文档:
单维数组实现了通用接口 IList<T>
、ICollection<T>
、IEnumerable<T>
、IReadOnlyList<T>
和 IReadOnlyCollection<T>
。这些实现在运行时提供给数组,因此,在 Array 类的声明语法中不会出现通用接口。
你能使用数组实现的接口中的所有成员吗?
不行。文档继续说明:
当你将一个数组强制转换为这些接口之一时,需要注意的关键是添加、插入或删除元素的成员会抛出NotSupportedException
异常。
这是因为(例如)ICollection<T>
有一个Add
方法,但是你不能向数组中添加任何内容。它会抛出异常。这是.NET Framework中早期设计错误的另一个例子,会在运行时抛出异常:
ICollection<Mammoth> collection = new Mammoth[10]; // Cast to interface type
collection.Add(new Mammoth()); // Run-time exception
由于 ICollection<T>
不是协变的(显而易见的原因),所以您不能这样做:
ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths; // Not allowed
当然,现在还有一个协变的
IReadOnlyCollection<T>
接口 在幕后也被数组实现了
1,但它只包含
Count
,因此用途有限。
基类 Array
如果数组是泛型的,我们是否仍然需要非泛型的 Array
类?
在早期阶段,我们需要。所有数组都通过它们的基类 Array
实现了非泛型 IList
、ICollection
和 IEnumerable
接口。这是给所有数组特定方法和接口的唯一合理方式,并且是使用 Array
基类的主要用途。枚举也是相同的选择:它们是值类型,但从 Enum
继承成员;委托也是如此,继承自 MulticastDelegate
。
现在支持泛型,是否可以删除非泛型的基类 Array
?
如果存在泛型类Array<T>
,则所有数组共享的方法和接口可以定义在该类上。然后您可以编写例如Copy<T>(T[] source, T[] destination)
而不是Copy(Array source, Array destination)
,这还具有一定的类型安全性。
但从面向对象编程的角度来看,拥有一个通用的非泛型基类Array
很好,可以用于引用任何类型的数组,而不管其元素的类型如何。就像IEnumerable<T>
继承自IEnumerable
(某些LINQ方法仍在使用它)。
Array基类是否可以派生自Array<object>
?
不行,这会创建一个循环依赖关系:Array<T> : Array : Array<object> : Array : ...
。此外,这将意味着您可以在数组中存储任何对象(毕竟,所有数组最终都将继承类型Array<object>
)。
未来
新的通用数组类型Array<T>
能够在不太影响现有代码的情况下添加吗?
不行。虽然语法可以进行调整,但现有的数组协变无法使用。
数组是.NET中的一种特殊类型。它甚至在公共中间语言中拥有自己的指令。如果.NET和C#的设计人员决定沿着这条路走,他们可以将T[]
语法作为Array<T>
的语法糖(就像T?
是Nullable<T>
的语法糖一样),并且仍然使用分配连续内存的特殊指令和支持。
但是,您将失去将Mammoth[]
数组转换为其基本类型之一Animal[]
的能力,类似于无法将List<Mammoth>
转换为List<Animal>
。但是,数组协变已经被破坏了,而且有更好的替代方案。
数组协变的替代方案?
所有的数组都实现了
IList<T>
接口。如果将
IList<T>
接口作为一个逆变接口进行处理,那么你就可以将任何数组
Array<Mammoth>
(或者其他任何列表)强制转换为
IList<Animal>
。然而,这需要重写
IList<T>
接口以删除所有可能更改底层数组的方法。
interface IList<out T> : ICollection<T>
{
T this[int index] { get; }
int IndexOf(object value);
}
interface ICollection<out T> : IEnumerable<T>
{
int Count { get; }
bool Contains(object value);
}
请注意,输入参数位置上的类型不能是
T
,否则会破坏协变性。但是,
object
对于
Contains
和
IndexOf
来说已经足够了,当传入错误类型的对象时,它们只会返回
false
。实现这些接口的集合可以提供自己的泛型
IndexOf(T value)
和
Contains(T value)
方法。
然后你就可以这样做:
Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths; // Covariant conversion
甚至有小的性能提升,因为运行时不需要检查分配的值是否与数组元素的实际类型兼容,当设置数组元素的值时。
我的尝试
我尝试了一下,如果在C#和.NET中实现Array<T>
类型,并结合上述真正的协变IList<T>
和ICollection<T>
接口,它可以很好地工作。我还添加了不变的IMutableList<T>
和IMutableCollection<T>
接口,以提供新的IList<T>
和ICollection<T>
接口缺少的变异方法。
我围绕它构建了一个简单的集合库,您可以从BitBucket下载源代码和编译二进制文件,或安装NuGet包:
M42.Collections - 专门的集合,具有比内置的.NET集合类更多的功能、特性和易用性。
1) 在 .Net 4.5 中,数组 T[]
通过其基类 Array
实现了以下接口:ICloneable
, IList
, ICollection
, IEnumerable
, IStructuralComparable
, IStructuralEquatable
;并且在运行时通过以下接口默默实现:IList<T>
, ICollection<T>
, IEnumerable<T>
, IReadOnlyList<T>
, 和 IReadOnlyCollection<T>
。
Array<T>
能提供什么样的功能,而List<T>
不能够提供呢? - D StanleyList<T>
、Array
和T[]
。(最后一个例子中,我指的是强类型数组,而不是“通用”数组) - D Stanley