小编典典

为什么数组不是通用类型?

c#

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

阅读 333

收藏
2020-05-19

共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<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类的声明语法中。


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

否。文档以以下注释继续:

将数组强制转换为这些接口之一时要意识到的关键是,添加,插入或删除元素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
ICollection并且
IEnumerable接口,可以通过它们的基类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 : ...。另外,这意味着您可以将 任何
对象存储在数组中(毕竟,所有数组最终都将继承自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那样,因为这会破坏协方差。但是,object对于ContainsIndexOf来说已经足够了,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>接口,以提供我的new
IList<T>ICollection<T>接口缺乏的突变方法。

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

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


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

2020-05-19