为什么 Array 不是泛型类型?

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> {
    
51314 次浏览

数组是一个历史类型,可以追溯到没有泛型的时代。

今天,使用 Array,然后是 Array<T>,然后是特定的类是有意义的;)

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

原因是在 C # 的第一个版本中没有出现泛型。

但我自己也想不出问题出在哪里。

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

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

都会被打破。

如果 MS 制造 C # 和。NET 从头再来一遍,然后可能会没有问题,使 Array成为一个通用类,但这不是现实。

[更新,新的见解,它觉得有些东西缺失,直到现在]

关于早先的答案:

  • 数组与其他类型一样是协变的。您可以使用协方差实现像‘ object [] foo = new string [5] ;’这样的东西,所以这不是原因。
  • 兼容性可能是不重新考虑设计的原因,但我认为这也不是正确的答案。

然而,我能想到的另一个原因是,数组是内存中一组线性元素的“基本类型”。我一直在考虑使用 Array < T > ,这也是您可能想知道为什么 T 是一个对象以及为什么这个“对象”甚至存在的地方?在这个场景中,T []只是我考虑的 Array < T > 的另一种语法,它与 Array 协变。由于这两种类型实际上是不同的,我认为这两种情况是相似的。

注意,基本 Object 和基本 Array 都不是 OO 语言的要求。C + + 就是一个很好的例子。没有这些基本构造的基本类型的警告是不能使用反射处理数组或对象。对于对象,你习惯于做一些让“对象”感觉自然的东西。实际上,如果没有数组基类,那么同样不可能执行 Foo ——虽然使用频率不高,但对于范例来说同样重要。

因此,在没有 Array 基类型的情况下使用 C # ,但是使用丰富的运行时类型(特别是反射)是不可能的。

更多细节..。

在哪里使用数组,为什么使用数组

对于像数组这样的基本类型,有一个基本类型可以用于很多事情,而且有很好的理由:

  • 简单的数组

是的,我们已经知道人们使用 T[],就像他们使用 List<T>一样。两者都实现了一组通用的接口,准确地说是: IList<T>ICollection<T>IEnumerable<T>IListICollectionIEnumerable

如果您知道这一点,就可以很容易地创建 Array。我们都知道这是真的,但这并不令人兴奋,所以我们继续..。

  • 创建集合。

如果你深入研究 List,你最终会得到一个 Array ——确切地说: 一个 T []数组。

为什么?虽然您可以使用指针结构(LinkedList) ,但它们是不一样的。列表是连续的内存块,通过连续的内存块获得速度。这有很多原因,但简单地说: 处理连续内存是处理内存的最快方式-甚至在您的 CPU 中有指令,使其更快。

细心的读者可能会指出这样一个事实,即您不需要一个数组,而是一个连续的类型为“ T”的元素块,IL 可以理解并处理这些元素。换句话说,您可以在这里去掉 Array 类型,只要您确保 IL 可以使用另一种类型来做同样的事情。

请注意,这里有值和类类型。为了保持尽可能好的性能,您需要将它们存储在您的块中,但是对于编组来说,这只是一个简单的需求。

  • 马歇尔。

编组使用所有语言都同意通信的基本类型。这些基本类型包括字节、整数、浮点、指针... 和数组。最值得注意的是数组在 C/C + + 中的使用方式,如下所示:

for (Foo *foo = beginArray; foo != endArray; ++foo)
{
// use *foo -> which is the element in the array of Foo
}

基本上,这会在数组的开始处设置一个指针,并递增指针(使用 sizeof (Foo)字节) ,直到到达数组的末尾。在 * foo 处检索元素-该元素获取指针‘ foo’所指向的元素。

再次注意,有值类型和引用类型。您实际上不希望 MyArray 只是将所有打包的内容存储为一个对象。实现 MyArray 变得非常棘手。

一些细心的读者可以在这里指出一个事实,即您在这里并不真正需要一个数组,这是真的。您需要一个连续的 Foo 类型的元素块——如果它是一个值类型,它必须作为(字节表示的)值类型存储在块中。

  • 多维数组

那么... 多维度呢?显然,规则不是非黑即白的,因为突然之间,我们不再有所有的基础类:

int[,] foo2 = new int[2, 3];
foreach (var type in foo2.GetType().GetInterfaces())
{
Console.WriteLine("{0}", type.ToString());
}

强类型刚刚从窗口中消失了,您最终得到的是集合类型 IListICollectionIEnumerable。嘿,那我们要怎么拿到尺寸呢?在使用 Array 基类时,我们可以这样做:

Array array = foo2;
Console.WriteLine("Length = {0},{1}", array.GetLength(0), array.GetLength(1));

但是如果我们看看像 IList这样的替代品,就没有等价物了。我们要怎么解决这个问题?这里应该引入 IList<int, int>吗?当然这是错误的,因为基本类型只是 int。那 IMultiDimentionalList<int>呢?我们可以这样做,并用 Array 中当前的方法填充它。

  • 数组的大小是固定的

您是否注意到对重新分配数组的特殊调用?这与内存管理有很大关系: 数组是如此低级,以至于它们不知道什么是增长或缩小。在 C 语言中,你可以使用“ malloc”和“ realloc”,你真的应该实现你自己的“ malloc”和“ realloc”来理解为什么固定的大小对于你直接分配的 所有东西是很重要的。

如果你看看它,只有一对夫妇的东西被分配在一个“固定”的大小: 数组,所有基本的值类型,指针和类。显然,我们处理数组的方式不同,就像我们处理基本类型的方式不同一样。

关于类型安全的一点注意事项

那么,一开始为什么需要这些“接入点”接口呢?

在所有情况下的最佳实践是为用户提供类型安全的访问点。这可以通过比较下面的代码来说明:

array.GetType().GetMethod("GetLength").Invoke(array, 0); // don't...

这样的代码:

((Array)someArray).GetLength(0); // do!

类型安全使您能够在编程时马虎。如果使用正确,编译器将发现错误,而不是在运行时发现错误。我再怎么强调这一点都不为过——毕竟,您的代码可能根本不会在测试用例中被调用,而编译器总是会对它进行计算!

把它们放在一起

所以... 让我们把它们放在一起,我们想要:

  • 强类型数据块
  • 连续存储其数据
  • IL 支持,以确保我们可以使用很酷的 CPU 指令,使它流血快
  • 一个公开所有功能的公共接口
  • 类型安全
  • 多维度
  • 我们希望将值类型存储为值类型
  • 和其他语言一样的编组结构
  • 以及固定的大小,因为这使得内存分配更加容易

这对于任何集合来说都是相当低级的要求... 它需要内存以某种特定的方式进行组织以及转换为 IL/CPU... 我想说它被认为是一种基本类型是有充分理由的。

历史

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

在 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类文档:

一维数组实现 IList<T>ICollection<T>IEnumerable<T>IReadOnlyList<T>IReadOnlyCollection<T>通用接口。实现在运行时提供给数组,因此,泛型接口不会出现在 Array 类的声明语法中。


可以使用数组实现的接口的所有成员吗?

不,文件继续这样说:

在将数组强制转换为这些接口之一时,需要注意的关键事项是,添加、插入或移除元素的成员抛出 NotSupportedException

这是因为(例如) ICollection<T>有一个 Add方法,但是您不能向数组中添加任何内容。它会抛出一个异常。中的早期设计错误的另一个示例。NET 框架,它会让你在运行时抛出异常:

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>接口,它也是通过 hood1下的数组实现的,但是它只包含 Count,所以它的用途有限。


基类 Array

如果数组是通用的,我们还需要非通用的 Array类吗?

在早期我们做了。所有数组实现了非通用的 IList, ICollection 通过其基类 ArrayIEnumerable 接口。这是为所有数组提供特定方法和接口的唯一合理方法,也是 Array基类的主要用途。对于枚举,您可以看到相同的选择: 它们是值类型,但是从 Enum继承成员; 以及从 MulticastDelegate继承的委托。

既然支持泛型,是否可以删除非泛型基类 Array

是的,所有数组共享的方法和接口可以在通用 Array<T>类上定义,如果它存在的话。然后,您可以编写,例如,Copy<T>(T[] source, T[] destination)而不是 Copy(Array source, Array destination),这样就增加了一些类型安全性的好处。

然而,从面向对象程序设计的角度来看,有一个通用的非泛型基类 Array是很好的,它可以用来引用 任何数组,而不管其元素的类型如何。就像 IEnumerable<T>如何从 IEnumerable继承(在一些 LINQ 方法中仍然使用 IEnumerable)一样。

Array基类能从 Array<object>派生出来吗?

不,这将创建一个循环依赖项: Array<T> : Array : Array<object> : Array : ...。另外,这意味着可以将 任何对象存储在数组中(毕竟,所有数组最终都将继承自 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,因为这会破坏协方差。但是,对于 ContainsIndexOf来说,object已经足够好了,当传递一个类型不正确的对象时,它们只返回 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 包:

与内置的. NET 集合类相比,具有更多功能、特性和易用性的专门集合。


中的数组 T[]。Net 4.5通过其基类 Array: Array3、 Array4、 Array5、 Array6、 Array7、 Array8实现; 并通过运行时无声地实现: Array9、 ICloneable0、 ICloneable1、 ICloneable2和 ICloneable3。

正如每个人所说-原来的 Array是非泛型的,因为没有泛型时,它出现在 v1。下面的推测是..。

要让“ Array”变得通用(现在这样就说得通了) ,你可以选择

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

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

    由于 Array是非常基本的类型,几乎所有现有代码(包括带有反射的自定义代码和带有本机代码和 COM 的编组)都使用它。因此,即使是版本之间的微小不兼容性(1.x-> 2.x 的。网络架构)会非常高。

因此,结果 Array类型是永远存在的。我们现在有 List<T>作为通用等价物被使用。

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

  • 即使今天的协方差特征从引入泛型的那一刻起就已经存在,它们也不足以用于数组。设计用于对 Car[]进行排序的例程将能够对 Buick[]进行排序,即使它必须将元素从数组复制到 Car类型的元素中,然后再将它们复制回来。将元素从 Car类型复制回 Buick[]实际上并不是类型安全的,但它很有用。我们可以定义一个协变数组单维数组接口,使排序成为可能[例如,通过包含一个‘ Swap (int firstIndex,int second Index)方法] ,但是很难做出像数组那样灵活的东西。

  • 虽然 Array<T>类型可能对于 T[]工作得很好,但是在泛型类型系统中没有办法为任意数量的下标定义一个包含 T[]T[,]T[,,]T[,,,]等的系列。

  • 没有办法进去。Net 表示两种类型应被视为相同的概念,这样类型为 T1的变量可以复制到类型为 T2的变量,反之亦然,两个变量都保存对同一对象的引用。使用 Array<T>类型的人可能希望能够将实例传递给需要 T[]的代码,并接受来自使用 T[]的代码的实例。如果旧样式的数组不能在使用新样式的代码之间传递,那么新样式的数组将是一个障碍,而不是一个特性。

也许有一些方法可以让类型系统运行在 Array<T>应该运行的状态下,但是这样的类型在许多方面的行为与其他泛型类型完全不同,而且由于已经有一个类型实现了所需的行为(即 T[]) ,因此还不清楚定义另一个类型会带来什么好处。

也许我遗漏了一些东西,但是除非数组实例被强制转换为 ICollection 或用作 IEnumable 等。那么使用 T 数组不会得到任何结果。

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