为什么Array不是泛型类型?

Ken*_*Kin 56 c# generics types language-design

Array 声明如下:

public abstract class Array
    : ICloneable, IList, ICollection, IEnumerable {
Run Code Online (Sandbox Code Playgroud)

我想知道为什么不是它:

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

  2. 如果它是泛型类型,我们还需要非泛型吗?如果我们愿意的话,它是否可以衍生出来Array<T>(如上所述)?

    public partial class Array: Array<object> { 
    
    Run Code Online (Sandbox Code Playgroud)

更新:

第二点对于不再使非泛型Array成为基本类型的假设也很重要.

Dan*_*ker 124

历史

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

回到C#1.0,​​他们主要从Java复制了数组的概念.当时Generics并不存在,但是创建者认为它们很聪明并且复制了Java数组所具有的破坏的协变数组语义.这意味着你可以在没有编译时错误的情况下完成这样的事情(但是运行时错误):

Mammoth[] mammoths = new Mammoth[10];
Animal[] animals = mammoths;            // Covariant conversion
animals[1] = new Giraffe();             // Run-time exception
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

数组实现通用接口

为什么不阵列实现通用IList<T>,ICollection<T>并且IEnumerable<T>接口?

由于运行时招每个数组T[] 执行IEnumerable<T>,ICollection<T>IList<T>自动.1Array课程文档:

在.NET Framework 2.0版,Array类实现IList<T>,ICollection<T>以及IEnumerable<T>通用接口.这些实现在运行时提供给数组,因此文档构建工具不可见.因此,通用接口不会出现在Array类的声明语法中.


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

不会.文档继续这句话:

将数组转换为其中一个接口时要注意的关键是添加,插入或删除元素的成员抛出IReadOnlyList<T>.

那是因为(例如)IReadOnlyCollection<T>有一个NotSupportedException方法,但你不能向数组添加任何东西.它会引发异常.这是.NET Framework中早期设计错误的另一个示例,它将在运行时向您抛出异常:

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

由于ICollection<T>不是协变的(出于显而易见的原因),你不能这样做:

ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths;     // Not allowed
Run Code Online (Sandbox Code Playgroud)

当然,现在有协变Add接口也由引擎盖1下的阵列实现,但它仅包含ICollection<T>它,因此它的用途有限.


基类 IReadOnlyCollection<T>

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

在早期我们做到了.所有数组都实现非泛型Array, ArrayIList通过其基类实现接口ICollection.这是为所有数组提供特定方法和接口的唯一合理方法,也是IEnumerable基类的主要用途.你看到枚举的相同选择:它们是值类型但是从中继承成员Array; 和继承自的代表Array.

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

是的,所有数组共享的方法和接口可以在泛型MulticastDelegate类中定义(如果它已经存在).然后你可以编写,例如,Array而不是Array<T>一些类型安全的额外好处.

但是,从面向对象编程的角度来看,拥有一个Copy<T>(T[] source, T[] destination)可用于引用任何数组的公共非泛型基类是很好的,无论其元素的类型如何.就像Copy(Array source, Array destination)继承自Array(在某些LINQ方法中仍然使用).

可以在IEnumerable<T>基类派生自IEnumerable

不,这会产生循环依赖:Array.此外,这意味着您可以将任何对象存储在数组中(毕竟,所有数组最终都将从类型继承Array<object>).


未来

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

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

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

但是,您将失去将数组T?转换为其基本类型之一的能力Nullable<T>,类似于您无法Mammoth[]转换的方式Animal[].但无论如何阵列协方差都被打破了,还有更好的选择.

阵列协方差的替代方案?

所有阵列都实现List<Mammoth>.如果将List<Animal>接口制作成适当的协变接口,那么您可以将任何数组IList<T>(或任何列表)转换为IList<T>.但是,这需要Array<Mammoth>重写接口以删除可能更改基础数组的所有方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

(请注意,参数上输入位置的类型不能IList<Animal>因为这将打破协方差.但是,IList<T>是配不上Tobject,谁也只是返回Contains时通过了一项不正确类型的对象.而实现这些接口的集合可以提供自己的通用IndexOffalse.)

然后你可以这样做:

Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths;    // Covariant conversion
Run Code Online (Sandbox Code Playgroud)

甚至有一个小的性能改进,因为在设置数组元素的值时,运行时不必检查指定的值是否与数组元素的实际类型兼容.


我刺伤了它

IndexOf(T value)如果它将在C#和.NET中实现,并结合上面描述的真实协变Contains(T value)Array<T>接口,我就会尝试这种类型如何工作,并且它工作得非常好.我还添加了不变量IList<T>ICollection<T>接口,以提供我的新IMutableList<T>IMutableCollection<T>接口所缺少的变异方法.

我围绕它构建了一个简单的集合库,您可以从BitBucket下载源代码和编译的二进制文件,或者安装NuGet包:

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


1)数组IList<T>中.NET 4.5工具通过它的基类ICollection<T>:T[],Array,ICloneable,IList,ICollection,IEnumerable; 默默通过运行时:IStructuralComparable,IStructuralEquatable,IList<T>,ICollection<T>,和IEnumerable<T>.

  • 好吧,当我阅读文件时,看起来我的眼睛欺骗了我,因为它们确实是LGPL. (2认同)

atl*_*ste 12

[更新,新见解,直到现在才觉得有些东西丢失]

关于早先的答案:

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

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

请注意,基本对象和基本数组都不是OO语言的要求.C++就是一个很好的例子.对于这些基本结构没有基本类型的警告是无法使用反射来处理数组或对象.对于你习惯于制作Foo的东西,让"对象"感觉自然.实际上,没有数组基类使得同样不可能做Foo - 这不是经常使用的,但对于范例同样重要.

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

更多细节......

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

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

  • 简单的数组

是的,我们已经知道人们使用T[],就像他们使用的那样List<T>.这两个实施一套通用的接口,确切的说:IList<T>,ICollection<T>,IEnumerable<T>,IList,ICollectionIEnumerable.

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

  • 创建集合.

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

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

细心的读者可能会指出这样一个事实,即你不需要一个数组,而是一个连续的"T"类型的元素,IL可以理解并可以处理它们.换句话说,你可以在这里摆脱数组类型,只要你确保IL可以使用另一种类型做同样的事情.

请注意,有值和类类型.为了保持最佳性能,您需要将它们存储在块中......但是对于编组它只是一个要求.

  • 编组.

编组使用所有语言都同意进行通信的基本类型.这些基本类型是诸如byte,int,float,pointer ...和array之类的东西.最值得注意的是数组在C/C++中的使用方式,如下所示:

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

基本上,这会在数组的开头设置一个指针并递增指针(使用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());
}
Run Code Online (Sandbox Code Playgroud)

强类型刚刚离开窗口,你最终得到了集合类型IList,ICollectionIEnumerable.嘿,那么我们应该如何获得这么大的尺寸呢?使用Array基类时,我们可以使用它:

Array array = foo2;
Console.WriteLine("Length = {0},{1}", array.GetLength(0), array.GetLength(1));
Run Code Online (Sandbox Code Playgroud)

......但如果我们看一下类似的替代品IList,就没有相应的东西.我们怎么解决这个问题?应该介绍IList<int, int>一下吗?当然这是错误的,因为基本类型是公正的int.怎么样IMultiDimentionalList<int>?我们可以这样做,并用当前在Array中的方法填充它.

  • 数组具有固定大小

您是否注意到有重新分配数组的特殊调用?这与内存管理有关:数组的级别太低,以至于它们无法理解增长或缩小的程度.在C中你会使用'malloc'和'realloc',你真的应该实现你自己的'malloc'和'realloc'来理解为什么确切地说固定大小对你直接分配的所有东西都很重要.

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

关于类型安全的附注

那么为什么首先需要这些所有这些"接入点"接口呢?

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

array.GetType().GetMethod("GetLength").Invoke(array, 0); // don't...
Run Code Online (Sandbox Code Playgroud)

像这样的代码:

((Array)someArray).GetLength(0); // do!
Run Code Online (Sandbox Code Playgroud)

类型安全使您在编程时可以马虎.如果使用正确,编译器会在找到错误时找到错误,而不是在运行时找到错误.我不能强调这是多么重要 - 毕竟,你的代码根本不会在测试用例中调用,而编译器总是会对它进行评估!

把它们放在一起

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

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

这对于任何集合来说都是相当低级别的要求......它需要以某种方式组织内存以及转换为IL/CPU ...我认为这是一个很好的理由它被认为是一种基本类型.

  • 是的,我们同意这一切.我的意思是`T [] == Array <T>:Array`,而不是`T []:Array <T>:Array`.是的,使用`Array <T>`而不是'T []`语法是我的_preference_,因为C#中的其他所有内容都是这样的.而今天你使用数组的工作要少得多,而不是`IEnumerable <T>`或`List <T>`.`Array <T>`很适合该列表.我的意思是类型安全:动物[动物] =新大象[10]; animals [0] = new Giraffe();`显示数组不提供其他语言构造(包括协变泛型)的编译时类型安全性.提前运行时异常! (2认同)

小智 11

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

今天,它将使意义有Array,那么Array<T>,那么具体类;)


JLR*_*she 5

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

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

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

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

Array ary = Array.Copy(.....);
int[] values = (int[])ary;
Run Code Online (Sandbox Code Playgroud)

会被打破.

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