【问题标题】:Why isn't Array a generic type?为什么 Array 不是泛型类型?
【发布时间】:2012-12-28 19:10:42
【问题描述】:

Array 被声明:

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

我想知道为什么不是:

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

  2. 如果它是一个泛型类型,我们还需要非泛型类型还是可以从Array&lt;T&gt; 派生?比如

    public partial class Array: Array<object> { 
    

【问题讨论】:

  • 你想从Array&lt;T&gt; 中得到什么,而List&lt;T&gt; 没有提供?
  • 应该没有问题,但是为什么要创建一个没有任何价值的类呢?您已经拥有List&lt;T&gt;ArrayT[]。 (最后一个例子是指强类型数组,而不是“通用”数组)
  • 不会引起问题;这是多余的和不必要的。没有它会给您带来问题吗?它会给你什么 List 还没有?还是只是语义?
  • D Stanley 的问题无关紧要,没有抓住重点,他的说法是错误的。幸运的是,Virtlink 的全面回答提供了正确且相关的信息。
  • Array 在 Swift 中非常通用,我认为它大量借鉴了 C#。因此,C# 数组似乎是一种因过早实现而陷入困境的语言,因为很多代码都基于旧样式。

标签: c# generics types language-design


【解决方案1】:

历史

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

在 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&lt;T&gt;ICollection&lt;T&gt;IEnumerable&lt;T&gt; 接口?

感谢运行时技巧,每个数组T[]确实自动实现IEnumerable&lt;T&gt;ICollection&lt;T&gt;IList&lt;T&gt;1来自Array class documentation

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


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

没有。文档继续此评论:

将数组转换为这些接口之一时要注意的关键是添加、插入或删除元素的成员会抛出 NotSupportedException

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

ICollection<Mammoth> collection = new Mammoth[10];  // Cast to interface type
collection.Add(new Mammoth());                      // Run-time exception

而且由于ICollection&lt;T&gt; 不是协变的(原因很明显),你不能这样做:

ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths;     // Not allowed

当然,现在还有协变 IReadOnlyCollection&lt;T&gt; interface,它也由底层数组实现1,但它只包含 Count,因此用途有限。


基类Array

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

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

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

是的,如果通用Array&lt;T&gt; 类存在的话,所有数组共享的方法和接口都可以定义。然后,您可以编写,例如,Copy&lt;T&gt;(T[] source, T[] destination) 而不是 Copy(Array source, Array destination),这会带来一些类型安全的额外好处。

然而,从面向对象编程的角度来看,最好有一个通用的非泛型基类Array,它可以用来引用任何数组,而不管它的类型是什么它的元素。就像 IEnumerable&lt;T&gt; 继承自 IEnumerable 一样(在某些 LINQ 方法中仍在使用)。

Array 基类是否可以派生自 Array&lt;object&gt;

不,这会产生循环依赖:Array&lt;T&gt; : Array : Array&lt;object&gt; : Array : ...。此外,这意味着您可以将 any 对象存储在数组中(毕竟,所有数组最终都将继承自类型 Array&lt;object&gt;)。


未来

是否可以添加新的泛型数组类型 Array&lt;T&gt; 而不会过多影响现有代码?

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

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

但是,您将无法将Mammoth[] 的数组转换为它们的基本类型之一Animal[],类似于您无法将List&lt;Mammoth&gt; 转换为List&lt;Animal&gt;。但是数组协方差无论如何都被打破了,还有更好的选择。

数组协方差的替代方法?

所有数组都实现IList&lt;T&gt;。如果IList&lt;T&gt; 接口被制成适当的协变接口,那么您可以将任何数组Array&lt;Mammoth&gt;(或任何列表)转换为IList&lt;Animal&gt;。但是,这需要重写 IList&lt;T&gt; 接口以删除所有可能更改底层数组的方法:

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

那么你可以这样做:

Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths;    // Covariant conversion

甚至还有很小的性能改进,因为在设置数组元素的值时,运行时不必检查分配的值是否与数组元素的实际类型兼容。


我的尝试

我尝试了如果在 C# 和 .NET 中实现这样的Array&lt;T&gt; 类型,并结合上面描述的真正的协变 IList&lt;T&gt;ICollection&lt;T&gt; 接口,它将如何工作,并且它工作得非常好。我还添加了不变的IMutableList&lt;T&gt;IMutableCollection&lt;T&gt; 接口来提供我的新IList&lt;T&gt;ICollection&lt;T&gt; 接口所缺乏的变异方法。

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

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


1) .Net 4.5 中的数组T[] 通过其基类Array 实现:ICloneableIListICollectionIEnumerableIStructuralComparableIStructuralEquatable;并默默地通过运行时:IList&lt;T&gt;ICollection&lt;T&gt;IEnumerable&lt;T&gt;IReadOnlyList&lt;T&gt;IReadOnlyCollection&lt;T&gt;

【讨论】:

  • 感谢 Odin,c# 设计者没有采用泛型的 java 方式,即 =The Erasure=。使用擦除 Array 在运行时与 Array 相同,但在所有其他方面都是如此可怕的事情。
  • 我不能在工作中使用它,因为该软件是专有的(不能分发源代码)。至少如果它是 LGPL 或具有 Apache2/MS-PL/MIT/BSD 的双重许可证,我可以使用它。
  • @Romoku 实际上它 LGPL。如果您发现有任何其他说明的文字,我想知道,以便我修复它。
  • 好吧,当我阅读这些文件时,我的眼睛似乎在欺骗我,因为它们确实是 LGPL。
  • @Virtlink:你应得的。
【解决方案2】:

也许我遗漏了一些东西,但除非数组实例被强制转换为或用作 ICollection、IEnumerable 等。那么你不会从 T 数组中获得任何东西。

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

【讨论】:

    【解决方案3】:

    [更新,新的见解,到现在为止感觉缺少了一些东西]

    关于较早的答案:

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

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

    请注意,基本对象和基本数组都不是面向对象语言的要求。 C++ 就是一个完美的例子。没有这些基本构造的基本类型的警告是无法使用反射处理数组或对象。对于您习惯于制作 Foo 的对象,这会使“对象”感觉自然。实际上,没有数组基类也同样不可能实现 Foo —— 这并不常用,但对范例同样重要。

    因此,让 C# 没有 Array 基类型,但具有丰富的运行时类型(尤其是反射)是 IMO 不可能的。

    更多细节......

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

    对于像数组这样基本的东西有一个基本类型用于很多事情,并且有充分的理由:

    • 简单数组

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

    如果您知道这一点,您可以轻松创建一个数组。我们也都知道这是真的,这并不令人兴奋,所以我们继续……

    • 创建集合。

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

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

    细心的读者可能会指出这样一个事实,即您不需要数组,而是 IL 可以理解并可以处理的“T”类型元素的连续块。换句话说,你可以在这里去掉 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());
    }
    

    Strong 类型刚刚被淘汰,您最终会得到集合类型IListICollectionIEnumerable。嘿,那我们应该如何获得尺寸呢?当使用 Array 基类时,我们可以这样使用:

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

    ...但是如果我们看看像IList 这样的替代品,没有等价物。我们将如何解决这个问题?应该在这里介绍一个IList&lt;int, int&gt; 吗?这肯定是错误的,因为基本类型只是intIMultiDimentionalList&lt;int&gt; 呢?我们可以这样做并用当前在 Array 中的方法填充它。

    • 数组具有固定大小

    您是否注意到有重新分配数组的特殊调用?这与内存管理有关:数组是如此低级,以至于它们不了解增长或收缩是什么。在 C 中,您将为此使用“malloc”和“realloc”,并且您确实应该实现自己的“malloc”和“realloc”,以了解为什么确切地具有固定大小对于您直接进行的所有事情都很重要分配。

    如果你看一下,只有几样东西是按“固定”大小分配的:数组、所有基本值类型、指针和类。显然我们处理数组的方式不同,就像我们处理基本类型的方式不同一样。

    关于类型安全的附注

    那么,为什么首先需要这些“接入点”接口?

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

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

    这样编码:

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

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

    把它们放在一起

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

    • 强类型数据块
    • 其数据连续存储
    • IL 支持确保我们可以使用酷炫的 CPU 指令,使其快速流血
    • 一个公开所有功能的通用接口
    • 类型安全
    • 多维度
    • 我们希望将值类型存储为值类型
    • 以及与任何其他语言相同的编组结构
    • 而且大小固定,因为这样可以更轻松地分配内存

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

    【讨论】:

    • 在我的答案中添加了更多内容......直到现在为止感觉好像缺少了一些东西。我问自己这个问题'具有 Array 的语言会有什么限制'并想出了这个问题。最后,如果我必须从头开始重新设计语言,我肯定会添加一个数组。出于这个原因,我强烈觉得这里的其他答案虽然感觉还可以,但都是不正确的。
    • 当然,你去 ;-) 我想我写了一本关于它的书,而不是一个简单的例子...... :-)
    • @StefandeBruijn 在我的帖子中“基本数组类型不正确”是什么意思?此外,关于您的编辑:要创建一个连续的 n 'T' 元素块而不使用数组,您需要创建一个具有 Tn 字段的类。您不能在 C# 或 CLR 的运行时执行此操作。此外,可以使用嵌套向量数组创建多维数组,在当前的 C# 中,T[][] 使用起来甚至比 T[,] 更快。
    • 我同意需要一个连续的元素块(一个数组),但它需要特殊的运行时支持并且有使用它的特殊说明。请注意,多维数组没有特殊指令,因此mutlidimensional arrays will be slower 比锯齿状数组。但是,在 C# 5 中没有 需要为数组提供特殊语法。对象实例化定义明确,但仅对数组不同。这是从不存在泛型开始的历史,但现在它污染了 C# 语言。顺便说一句,T[] 数组不是类型安全的。
    • 是的,我们同意这一切。我的意思是T[] == Array&lt;T&gt; : Array,而不是T[] : Array&lt;T&gt; : Array。是的,使用Array&lt;T&gt; 而不是T[] 语法是我的偏好,因为C# 中的所有其他内容也都以这种方式工作。而今天,与IEnumerable&lt;T&gt;List&lt;T&gt; 相比,您使用数组的工作要少得多。 Array&lt;T&gt; 非常适合该列表。我对类型安全的意思是:Animal[] animals = new Elephant[10]; animals[0] = new Giraffe(); 表明数组不提供其他语言构造(包括协变泛型)所提供的编译时类型安全。运行时异常提前!
    【解决方案4】:

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

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

    • 1234563 T[,,,] 等用于任意数量的下标。 1234563两个变量都持有对同一个对象的引用。使用Array&lt;T&gt; 类型的人可能希望能够将实例传递给需要T[] 的代码,并接受来自使用T[] 的代码的实例。如果旧样式的数组无法在使用新样式的代码之间传递,那么新样式的数组将更像是一个障碍而不是一个特性。

    可能有一些方法可以让类型系统发挥作用,以允许类型 Array&lt;T&gt; 表现得应该如此,但这种类型的行为方式与其他泛型类型完全不同,因为已经有一个实现所需行为的类型(即T[]),尚不清楚定义另一个会带来什么好处。

    【讨论】:

    • @KenKin:没有办法在 .net 类型系统中定义真正的数组数组;尝试这样做将改为定义数组的 references 数组。如果可以使用整数和Type 参数定义泛型类型,并且如果存在数组值类型ValueArray&lt;size,T&gt;,那么Int[3,5] 在类型系统中可能可以表示为大小为3 的Array&lt;ValueArray&lt;5,Int&gt;&gt;,但是框架中不存在此类设施。 C# 可以在“裸机”上实现固定数组,但框架不理解它们,也无法验证它们的用法。
    • @KenKin: C# Foo[][] 相当于 C *Foo[],而 C# Bar[,] 相当于 C Bar[][]
    【解决方案5】:

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

    要使“Array”通用(现在可以理解),您可以

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

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

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

    因此,Array 类型将永远存在。我们现在将 List&lt;T&gt; 作为通用等效项来使用。

    【讨论】:

    • 您似乎认为T[]Array 是等价的,但事实并非如此。而List&lt;T&gt;不是数组T[] 或任何数组Array 的通用等价物。
    • @Virtlink, On List vs.Array - 为什么你认为List&lt;T&gt;不是“一个数组概念”?从我的角度来看,T[](通过Array 实现)和List&lt;T&gt; 都提供了元素的有序序列,并保证了 O(1) 索引。列表更灵活。是的 List 并非在所有可以使用 T[] 的情况下都可用(例如 PInvoke),但这不是我关心的事情。
    • @Virtlink on Array vs T[]: Array 本质上是T[] 的泛型实现(在某种意义上“使用与泛型相同的概念”) - 所以是的,到在某种程度上我对待他们是同等的。我对原始问题的理解是“当真正的泛型Array&lt;T&gt; 可以用来进行更直接的映射时,为什么非泛型类之上还有语法糖(T[])”——可能是错误的/不相关的......跨度>
    • T[] 是一个具有协变类型参数T 的泛型数组(尽管与当今C# 中使用的泛型不同),但Array 与此处无关。如果所有数组T[] 直接从Object 继承,它也会同样有效。它们不是等价的。数组是一个基本概念,可为您提供连续的内存区域。这导致 O(1) 索引以及列表 也恰好 具有 O(1) 索引的事实并不使它们等效,当然也不会使列表等效于固定大小数组。
    • @Virtlink 好点 - 我们的定义非常不同,您可能应该编写自己的答案,这很可能会更有趣/有用。我对Array 的唯一目的是实现T[] 的理解,我看到它与您的“Array 恰好用于实现T[]”有什么不同。并且对于“作为基本概念的数组”与“可索引的元素序列”与“固定大小的连续内存块”不同......
    【解决方案6】:

    所以我想知道为什么不是:

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

    但我自己无法弄清楚问题出在哪里。

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

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

    会坏掉的。

    如果 MS 从头开始​​重新制作 C# 和 .NET,那么将 Array 制作为泛型类可能没有问题,但事实并非如此。

    【讨论】:

    • 我不确定我是否一定理解他的回答。如果反射不能很好地处理泛型类,那么我认为让反射更加通用将是答案,而不是将数组归类为非泛型类。但是,如果您认为他的问题能更好地回答您的问题,那么您可以接受它。
    【解决方案7】:

    兼容性。 Array 是一种历史悠久的类型,可以追溯到没有泛型的时代。

    今天有Array,然后是Array&lt;T&gt;,然后是特定的类是有意义的;)

    【讨论】:

    • 我会借此机会问一下,msdn.microsoft.com/en-us/library/system.array.aspx 断言“因此充当公共语言运行时中所有数组的基类。”。通过“作为基类”,文档是否意味着“是基类”?
    • 不,这是它不通用的原因。无论如何,它实际上是子类化的——只是不是通过正常的基因机制。它是古老的。 .NET 1.0。现在很难改变。
    • 是的。这简直就是历史。遗憾的是,很多旧代码库都忽略了泛型。
    • 是和不是。我预计会有很多更棘手的代码被破坏。反思明智,人们对层次结构做出假设。
    • @Eve:框架包含一些“特殊”类型,它们的后代被认为具有某些特征特殊类型本身缺乏。例如,从System.Enum 派生的每个类型都是以某种整数类型作为其基本表示形式的值类型,但System.Enum 本身是一个类类型。同样,System.ValueType 的派生词除了 System.Enum 之外是值类型,但 System.ValueType 本身是类类型。 System.Array 也存在类似的情况。每个合法的派生类型都有一个索引器,但System.Array 没有。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-24
    • 2018-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-30
    相关资源
    最近更新 更多