历史
如果数组变成泛型会出现什么问题?
在 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 中引入了泛型,但没有协变/逆变泛型类型。如果数组是通用的,那么你不能将Mammoth[] 转换为Animal[],这是你以前可以做的事情(即使它被破坏了)。所以使数组泛型会破坏很多代码。
仅在 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 class documentation:
一维数组实现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> interface,它也由底层数组实现1,但它只包含 Count,因此用途有限。
基类Array
如果数组是泛型的,我们还需要非泛型的Array 类吗?
在早期,我们这样做了。所有数组都实现非泛型IList,
ICollection 和
IEnumerable 通过它们的基类 Array 进行接口。这是为所有数组提供特定方法和接口的唯一合理方式,也是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 : ...。此外,这意味着您可以将 any 对象存储在数组中(毕竟,所有数组最终都将继承自类型 Array<object>)。
未来
是否可以添加新的泛型数组类型 Array<T> 而不会过多影响现有代码?
没有。虽然可以使语法适合,但不能使用现有的数组协方差。
数组是 .NET 中的一种特殊类型。它甚至有自己的通用中间语言指令。如果 .NET 和 C# 设计者决定走这条路,他们可以为Array<T> 制作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> 接口所缺乏的变异方法。
我围绕它构建了一个简单的集合库,你可以download the source code and compiled binaries from 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>。