历史
如果数组成为泛型类型会出现什么问题?
回到 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>
自动实现。1从类文档:ICollection<T>
IList<T>
Array
一维数组实现IList<T>
、ICollection<T>
、IEnumerable<T>
和泛型接口。这些实现在运行时提供给数组,因此,泛型接口不会出现在 Array 类的声明语法中。IReadOnlyList<T>
IReadOnlyCollection<T>
你能使用数组实现的接口的所有成员吗?
不,文档继续以下评论:
将数组强制转换为这些接口之一时要注意的关键是添加、插入或删除元素的成员 throw 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
类吗?
在早期,我们做到了。所有数组都通过它们的基类
实现非泛型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 : ...
. 此外,这意味着您可以将任何对象存储在数组中(毕竟,所有数组最终都将继承自 type 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
因为这会破坏协方差。但是,对于and来说object
已经足够了,当传递一个不正确类型的对象时会返回。实现这些接口的集合可以提供它们自己的泛型和.)Contains
IndexOf
false
IndexOf(T value)
Contains(T value)
然后你可以这样做:
Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths; // Covariant conversion
甚至还有很小的性能改进,因为在设置数组元素的值时,运行时不必检查分配的值是否与数组元素的实际类型兼容。
我的刺
Array<T>
如果这种类型在 C# 和 .NET 中实现,结合上面描述的真正的协变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>
.