为什么Array不是泛型类型?

时间:2013-01-14 19:13:34

标签: c# generics types language-design

宣布

Array

public abstract class Array
    : ICloneable, IList, ICollection, IEnumerable {

我想知道为什么不呢:

public partial class Array<T>
    : ICloneable, IList<T>, ICollection<T>, IEnumerable<T> {
  1. 如果它被声明为泛型类型会有什么问题?

  2. 如果它是泛型类型,我们是否还需要非泛型类型,还是来自Array<T>?如

    public partial class Array: Array<object> { 
    

6 个答案:

答案 0 :(得分:116)

历史

  

如果数组成为泛型类型会出现什么问题?

回到C#1.0,​​他们主要从Java复制了数组的概念。当时Generics并不存在,但是创建者认为它们很聪明并且复制了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类吗?

在早期我们做到了。所有数组都实现非泛型IListICollectionIEnumerable通过其基类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 : ...。此外,这意味着您可以将任何对象存储在数组中(毕竟,所有数组最终都将从类型Array<object>继承)。


未来

  

是否可以添加新的通用数组类型Array<T>而不会过多影响现有代码?

没有。虽然可以使语法适合,但是不能使用现有的数组协方差。

数组是.NET中的特殊类型。它甚至在通用中间语言中有自己的指令。如果.NET和C#设计师决定走这条路,他们可以为T[]制作Array<T>语法语法糖(就像T? Nullable<T>的语法糖一样{{1} }),仍然使用在内存中连续分配数组的特殊指令和支持。

但是,您将无法将Mammoth[]数组投射到其基本类型之一Animal[],类似于您无法将List<Mammoth>投射到{{1} }}。但无论如何阵列协方差都被打破了,还有更好的选择。

  

数组协方差的替代方法?

所有数组都实现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对于objectContains来说已经足够了传递一个不正确类型的对象时会返回IndexOf。实现这些接口的集合可以提供它们自己的通用falseIndexOf(T value)。)

然后你可以这样做:

Contains(T value)

甚至可以提高性能,因为在设置数组元素的值时,运行时不必检查指定的值是否与数组元素的实际类型兼容。


我刺伤了它

如果它将在C#和.NET中实现,并结合上面描述的真正的协变Array<Mammoth> mammoths = new Array<Mammoth>(10); IList<Animals> animals = mammoths; // Covariant conversion Array<T>接口,我会尝试这样的IList<T>类型是如何工作的,而且效果很好。我还添加了不变的ICollection<T>IMutableList<T>接口,以提供我的新IMutableCollection<T>IList<T>接口缺少的变异方法。

我围绕它构建了一个简单的集合库,您可以download the source code and compiled binaries from BitBucket或安装NuGet包:

  

M42.Collections - 比内置.NET集合类具有更多功能,特性和易用性的专用集合。


1 ).Net 4.5中的数组ICollection<T>通过其基类T[]实现:ICloneableIList,{{3} },ICollectionIEnumerableIStructuralComparable;并默默地通过运行时:IStructuralEquatableIList<T>ICollection<T>IEnumerable<T>IReadOnlyList<T>

答案 1 :(得分:11)

兼容性。数组是一种历史类型,可以追溯到没有泛型的时候。

今天有Array,然后Array<T>,然后是特定的类是有道理的;)

答案 2 :(得分:5)

  

因此,我想知道为什么不是:

原因是第一版C#中没有泛型。

  

但我无法弄清楚自己会出现什么问题。

问题在于它会破坏使用Array类的大量代码。 C#不支持多重继承,所以像这样的行

Array ary = Array.Copy(.....);
int[] values = (int[])ary;

会被打破。

如果MS从头开始重新编写C#和.NET,那么将Array作为泛型类可能没有问题,但这不是现实。

答案 3 :(得分:3)

除了人们提到的其他问题之外,尝试添加通用Array<T>会带来一些其他困难:

  • 即使今天的协方差特征从引入仿制药的那一刻起就存在,它们对阵列来说还不够。设计用于对Car[]进行排序的例程将能够对Buick[]进行排序,即使它必须将数组中的元素复制到Car类型的元素中,然后将其复制回来。将元素从Car类型复制回Buick[]并不是真正的类型安全,但它很有用。人们可以定义一个协变阵列的单维阵列接口,以便使分类成为可能[例如,通过包含`Swap(int firstIndex,int secondIndex)方法],但是很难做出像数组一样灵活的东西。

  • 虽然Array<T>类型可能适用于T[],但通用类型系统中无法定义包含T[],{{对于任意数量的下标,1}},T[,]T[,,]等。

  • .net中没有任何方法可以表达两种类型应该被视为相同的概念,这样类型T[,,,]的变量可以复制到T1类型的变量中,并且反之亦然,两个变量都持有对同一对象的引用。使用T2类型的人可能希望能够将实例传递给期望Array<T>的代码,并接受使用T[]的代码中的实例。如果无法将旧样式数组传递给使用新样式的代码,那么新样式数组将成为一个障碍而不是功能。

可能有一些方法可以使类型系统允许类型T[]表现得如此,但是这种类型的行为方式与其他泛型类型完全不同,并且因为存在已经是一种实现所需行为的类型(即Array<T>),不清楚定义另一种行为会产生什么好处。

答案 4 :(得分:2)

正如大家所说 - 原始Array是非泛型的,因为在v1中存在时没有泛型。猜测如下......

要使“数组”通用(现在才有意义),你可以

  1. 保留现有Array并添加通用版本。这很好,但是“阵列”的大多数用法都会随着时间的推移而逐渐增加,这很可能是因为实现了相同概念List<T>的更好实现。此时添加“不增长的元素的顺序列表”的通用版本看起来不太吸引人。

  2. 删除非泛型Array并使用相同的接口替换为通用Array<T>实现。现在,您必须为旧版本编译已编译的代码,以使用新类型而不是现有的Array类型。虽然框架代码支持这种迁移是可能的(也很可能很难),但总有很多代码由其他人编写。

    由于Array是非常基本的类型,因此几乎所有现有代码(包括使用本机代码和COM进行反射和编组的自定义代码)都使用它。因此,版本之间甚至微小不兼容的价格(.x框架的1.x - > 2.x)将非常高。

  3. 因此结果Array类型永远存在。我们现在将List<T>作为通用等效项使用。

答案 5 :(得分:0)

也许我错过了一些东西,但除非将数组实例转换为ICollection,IEnumerable等,否则你就不会获得任何T数组。

阵列很快,并且已经是类型安全的,不会产生任何装箱/拆箱开销。