70

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> { 
    
4

7 回答 7

149

历史

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

回到 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和 接口。这是为所有数组提供特定方法和接口的唯一合理方式,也是基类的主要用途。您会看到枚举的相同选择:它们是值类型,但从;继承成员。以及继承自.ICollectionIEnumerableArrayArrayEnumMulticastDelegate

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已经足够了,当传递一个不正确类型的对象时会返回。实现这些接口的集合可以提供它们自己的泛型和.)ContainsIndexOffalseIndexOf(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[]通过其基类实现ArrayICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable; 并默默地通过运行时:IList<T>, ICollection<T>, IEnumerable<T>, IReadOnlyList<T>, 和IReadOnlyCollection<T>.

于 2013-03-22T01:17:44.963 回答
15

【更新,新的见解,到现在感觉还少了点什么】

关于前面的回答:

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

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

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

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

所以更多细节......

数组在哪里使用以及为什么它们是数组

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

  • 简单数组

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

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

  • 创建集合。

如果你深入研究 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());
}

强类型刚刚消失,你最终得到了集合类型IListICollectionIEnumerable。嘿,那我们应该如何获得尺寸呢?当使用 Array 基类时,我们可以这样使用:

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

...但是如果我们看一下类似的替代方案IList,则没有等价物。我们将如何解决这个问题?这里应该介绍一个IList<int, int>?这肯定是错误的,因为基本类型是 just 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……我想说它被认为是基本类型是有充分理由的。

于 2013-01-15T19:15:31.247 回答
12

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

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

于 2013-01-14T19:15:41.597 回答
5

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

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

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

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

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

会坏掉的。

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

于 2013-01-14T19:31:32.743 回答
3

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

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

  • 虽然一个Array<T>类型可能适用于 a T[],但在泛型类型系统中没有办法为任意数量的下标定义一个包含T[], T[,], T[,,],等的族。T[,,,]

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

可能有一些方法可以让类型系统按其Array<T>应有的方式运行,但这种类型的行为方式与其他泛型类型完全不同,因为已经有一种类型实现了所需的行为(即T[]),尚不清楚定义另一个会带来什么好处。

于 2013-03-25T16:28:40.163 回答
2

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

要使“Array”通用(现在有意义),您可以

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

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

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

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

于 2013-03-22T02:37:17.703 回答
0

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

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

于 2013-03-26T23:28:38.197 回答