数组未记录






4.86/5 (179投票s)
2003年1月5日
22分钟阅读

444718
详细探讨数组和 ArrayList 的实现。
引言
本文深入探讨了公共语言运行时和 .NET Framework 中的数组。本研究详细介绍了数组的实现并描述了高效使用它们的方法。
这是我“未公开”系列中的第二篇文章,前一篇是关于字符串的文章。撰写本系列文章的目的是帮助我理解如何高效地使用 C# 开发一个严肃的商业应用程序。
我是一名 Excel 的前微软开发人员,现在创办了自己的软件公司,开发采用人工智能的应用程序。自从上个月我发表了关于未公开字符串的文章后,我实际上开始为微软做一些合同工作;我目前正在为 Windows 团队工作,简直不敢相信下一代 .NET Framework 有如此多的出色进展。但是,因为我签署了保密协议,所以我将所有信息限制在公开可用信息范围内,不会讨论未发布的产品。
背景
Array 类是编译器和运行时使用的数组的基类。与字符串一样,数组(包括派生类)是唯一长度可变的类型。数组的秩是数组的维数。数组维度下限是该维度数组的起始索引;多维数组的每个维度可以有不同的边界。
在内部,运行时维护两种独立的数组实现——优化的 SZARRAYS
和通用数组,我将它们称为 MDARRAYS
(用于多维数组)(尽管 MDARRAY
可以是一维的)。SZARRAYs
是基于零且一维的。另一方面,ARRAYs
是多维的和/或具有非零下限。SZARRAYS
比 ARRAYs
更常见,因此经过高度优化。下表详细说明了两者的区别。
SZARRAYS | MDARRAYS | |
描述 | 一维,基于零 | 多维和/或非零基 |
C# 语法 | object[] -- 普通数组 object[][] -- 交错数组 |
object [,] -- 多维 |
CLS 兼容 | 是 | 否,如果下限非零 |
IL 优化 | 是。IL 包含专门的指令,如 ldelem 和 stelem 来处理这些数组。 | 否,在版本 1.0 中。访问和修改通过函数发生。 |
方法优化 | 是。基本类型数组有专门的方法,可以高效执行而无需装箱。 | 否,在版本 1.0 中。所有类型的数组都使用相同的通用代码实现,该代码将所有元素转换为对象。值类型在 sort、reverse 等函数中被反复装箱和拆箱。 |
基本大小(不包括 8 字节的虚表和对象头) | 值类型为 4 字节 引用类型为 8 字节 |
4-8 字节 + 8 * 秩 |
JIT 优化 | JIT 执行范围检查消除。 | 无特殊 JIT 优化。添加额外的代码以对每个维度执行范围检查。 |
SZARRAYS
的性能远超 MDARRAYS
,因此,为了性能,包含其他 SZARRAYS
的交错数组(本身也是 SZARRAYS
)远优于常规多维数组。请记住,交错数组不兼容 CLS,可能无法跨语言工作。
在 CLR 1.0 版本中,每个堆分配的对象都包含一个 4 字节的对象头和 4 字节的方法表指针,因此每个数组都有这个初始开销;此外,SZARRAYS
和 MDARRAYs
都包含以下内部字段。
变量 | 类型 | 描述 |
数组长度 | int |
这是数组中元素的实际数量。 |
元素类型 | 类型 |
可选。根据源代码,此字段仅在数组由“指针”组成时才存在。在这种情况下,“指针”似乎指的是对象引用,而不是非托管指针。 |
MDARRAYS 在内部包含这些额外的字段。
变量 | 类型 | 描述 |
Bounds[秩] |
int[] |
可选。数组中元素的数量 |
LowerBound[秩] |
int[] |
可选。有效索引为 lowerBounds[i] <= index[i] < lowerBounds[i] + bounds[i] |
因此,常规数组中的每次访问都必须检查几个内部成员。为了实现高性能,多维数组有两种可能的方法:上述“交错”C# 数组或在 SZARRAYs
中手动计算。例如,(i-lowerbound1) * rowcount + (j-lowerbound2)
将定位元素 [i,j]。
非基于零的数组与多维数组具有相同的缺点,因为它们都是 MDARRAYS
。它们不仅没有优化,而且由于测试较少和边界问题,质量问题也更多。
例如,具有负下限的数组会产生有趣的错误。`array.IndexOf` 通常在失败时返回 -1;然而,对于具有负下限的数组,失败是 int32.MaxValue。但是,相同的数组将导致 `IList.Contain` 函数在缺少项时始终返回 true,因为该函数调用 `IndexOf` 并假定它将返回小于下限的失败值。此外,`array.BinarySearch` 在处理负下限时也存在问题。当 `array.BinarySearch` 失败时,它会返回一个负值,该值是插入项位置的补码。
如果两个数组具有相同的秩(维数)和相同的元素类型,则它们被认为是同一类型。与 C/C++ 不同,每个维度的上限和下限不被考虑,甚至多维数组的内部维度也不被考虑。诸如 Array.Copy
之类的方法通过将每个多维数组视为一个大型扁平一维数组,来操作相同类型的异构多维数组。不同秩的交错数组也是不同类型;这仅仅是因为这些数组具有不同的元素类型,尽管这些元素都是数组。有趣的是,表示 Array 基类的 Type 对象对于 Type.IsArray
返回 false,对于 Type.GetElementType
返回 null。当然,Array 类是抽象的,通常不能在不继承的情况下实例化。
除了基大小之外,MDARRAYs
和 SZARRAYS
都将包含值类型的连续内联结构和对象和字符串等引用类型的连续指针。引用类型还有一个位于数据之前的元素类型字段;它似乎是多余的,因为数组的方法表可以提取必要的类型信息。元素类型作为字段的存在允许快速提取元素类型信息,而无需虚拟调用间接和函数调用,这对于数组协变等特性很重要。
如果数据是值类型,则元素的尺寸将与该值类型相同。引用类型占用 IntPtr.Size
字节。对于 Win32,IntPtr.Size
为 4 字节,对于 Win64 为 8 字节。根据文档,这应该与 void * 的本机大小相等,但在 Rotor 的非 Win32 版本(如 Mac 和 UNIX)中,无论 CPU 如何,它始终是 8 字节。
类型 | 元素大小(字节) |
bool | 1 |
byte | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
double | 8 |
decimal | 16 |
字符串 | IntPtr.Size |
object | IntPtr.Size |
接口 | IntPtr.Size |
与字符串这种可变大小对象不同,所有内部数组字段均未在反射中公开。访问这些内部字段需要使用非安全代码。由于这些字段已经通过公共方法和属性公开,因此提供任何像我在“未公开字符串”中所做的那样用于此类操作的源代码将是多余的。
在编程上,一个数组是 SZARRAY,当且仅当 array.Rank==1 && array.GetLowerBound(0)==0
。如果 (elementType = array.GetType().GetElementType()) && elementType.IsSubclassOf(typeof(ValueType)) && elementType != typeof(Enum) && elementType != typeof(ValueType)
,则数组包含值类型成员。讽刺的是,Enum[]
和 ValueType[]
都不是值类型数组;它们只是包含对装箱值类型元素的引用的数组。实际上,最后一个条件是多余的,因为 ValueType 或任何其他类型不能是其自身的子类。
动态 ArrayList
ArrayList 是一个非常有用的类,用于处理动态数组。然而,它也服务于更广泛的目的——封装集合类并允许对这些类执行特殊操作。
ArrayList
将构造一个数组对象并直接修改它。默认情况下,ArrayList 将创建一个包含 16 个元素的数组。ArrayList
类的以下成员按以下顺序。
变量 | 类型 | 描述 |
_items | object[] |
基数组。 |
_size | int |
数组列表的当前大小。 |
_version | int |
每次修改后版本都会递增。这用于表示对列表早期版本的视图或枚举器进行的任何操作都应失败。 |
一个 ArrayList 总共消耗 20 字节(8 字节对象开销 + 12 字节实例信息),不包括底层数组的空间。
如果数组需要超出其容量增长,将构造一个新数组,其容量是之前的两倍或新的所需大小,受最大容量限制。这种方法的时间复杂度为 O(3n),是线性时间。将数组以固定量而非百分比增长的替代方法会导致二次时间性能,O(n^2)。为了获得最佳性能,如果已知大小,应预分配数组列表以减少不必要的复制。
完成 ArrayList 大小的压缩需要调用 TrimToSize
,这将实际执行另一个复制操作。如果所有元素都已添加且不需要进一步扩展,则通过调用 arraylist.ToArray(Type type)
提取类型安全数组在性能和内存方面都将更优化。
要完全释放数组空间,需要调用 `Clear`(唯一的替代方法是使用 `RemoveRange` 释放所有元素),然后调用 `TrimToSize`。讽刺的是,将 arraylist 容量设置为小于 16 会比使用容量为 0 占用更少的空间,因为容量为 0 会自动使用默认容量 16。
ArrayLists
无法完全替代数组。(我怀疑它们的性能高于前面提到的多维数组,但我将在不久的将来在另一篇专门讨论性能的文章中确定这一点。)
数组 | 数组列表 | |
内存要求 | 值类型的紧凑内联数据。 数据的对象引用。 |
基于对象的数组。 (值类型每个元素会产生额外的 12 字节开销——4 字节用于对象引用,8 字节用于装箱引入的对象头) |
性能 | 优化的 IL 指令。范围检查消除。 | 间接引用 |
固定大小 | 动态 | |
随机访问 | 在所有先前索引中的元素都被添加之前,对索引元素的访问是禁止的。解决此问题的方法是使用静态 ArrayList.Repeat(null, initial length) 方法构造一个 ArrayList。 |
在数组和 ArrayList 之间来回转换相当简单。数组可以使用 ArrayList.Adapter(array)
转换为 ArrayList。ArrayList 可以使用 ToArray()
或 ToArray(type)
转换为紧凑数组。
您还可以通过调用 (object[]) sb.GetType().GetField("_items", BindingFlags.NonPublic|BindingFlags.Instance).GetValue(arrayList)
访问 ArrayList 的底层数组。这并不是 ToArray() 的完美替代品,因为数组长度是 ArrayList 的容量而不是其计数。通过存储和重用对 GetField 的调用中的 FieldInfo 对象,可以消除任何内存和时间开销。
无需 ArrayList 的数组操作
手动调整数组大小
数组可以手动调整大小。这是一个应该由 Array 类提供的有用函数。要手动模拟 ArrayList 的行为,应在任何可能无效的索引之前调用 ensure。public static Array Resize(Array array, int newSize) { Type type = array.Type; Array newArray = Array.CreateInstance(type.GetElementType(), newSize); Array.Copy(array, 0, newArray, 0, Math.Min(newArray.Length, newSize))l return newArray; }Resize 方法使用 Array.CreateInstance 进行后期绑定构造。
数组移动
为了在数组中手动移动元素,Array 提供了一个通用的 Copy 函数,用于将数据从一个数组复制到另一个数组。此函数也适用于同一数组。范围检查只执行一次。在同一数组中,复制行为类似于 C 标准库的 memmove 函数,而不是 memcpy。
InsertHelper 函数移动数组内容,以便在 index 位置为 count 个元素腾出空间。任何靠近末尾的元素都会向右移出数组并消失。同样,RemoveHelper 方法从指定位置移除元素,将尾随元素向左移动。
public static Array InsertHelper(Array array, int index, int count) { Array.Copy(array, index, array, index+count, array.Length-(index+count)); array.Clear(index, count); } public static Array RemoveHelper(Array array, int index, int count) { int copy = ; Array.Copy(array, index+count, array, index, array.Length - (index+count)); array.Clear(array.Length - count, count); }
Buffer 类还提供了有用的函数,如 GetByte、SetByte、ByteLength 和 BlockCopy,用于操作不包含任何内部对象引用的值类型数组。事实上,Buffer 类会忽略元素的类型,因为它将每个数组视为一个字节范围。不同值类型的数组可以相互复制,因此,例如,浮点值可以叠加到整型数据上,反之亦然。要使用此类别,使用 sizeof 关键字或 Marshal.SizeOf 来获取值类型的精确大小会很有帮助。
在多维数组内部复制元素时,数组的行为就像一个长一维数组,其中行(或列)在概念上是首尾相连的。例如,如果一个数组有三行(或列),每行(或列)有四个元素,从数组开头复制六个元素将复制第一行(或列)的所有四个元素以及第二行(或列)的前两个元素。
位数组
一个不应忘记的类是类似 Pascal 集合的 BitArray,它的工作方式令人惊讶地像代码中的布尔数组。在 .NET 中,布尔值占用一个字节,虽然比 int 大小的 C++ 布尔值更紧凑,但在数组中仍然相当浪费。
BitArray 被实现为 Int32 数组,每个 Int32 包含 32 位。使用 int 数组而不是字节数组,可以一次访问和修改 32 位而不是 8 位,从而在许多操作中将性能提高四倍。
此外,BitArray 的运算符包括 And、Or、Xor 和 Not。还有一个相关的 BitVector32,它将使用一个 int 来表示一个小集合。数组强制转换与转换
数组协变:数组的转换
引用类型的数组支持一种称为数组协变的功能,它模拟了 C++ 将一种类型的指针数组转换为另一种类型的能力。如果两种类型之间存在某种内置转换(显式或隐式),则可以在编译时将一个数组转换为不同类型的另一个数组。这两个数组也必须具有相同的秩。数组被重新解释,转换过程中没有发生底层物理更改。
如果转换是隐式的(即,转换前数组的元素类型正在转换为它支持的接口或基类型),则不需要强制转换,也不会执行运行时检查。如果转换是显式的(转换是从接口到另一种类型,从基类型到派生类型,或者从基类型到该基类型不直接支持的接口),则需要显式强制转换并执行运行时检查。
正如前面提到的,每个引用数组都有一个底层元素类型,该类型在整个转换过程中保持不变。执行运行时检查以确保底层元素类型与正在重新解释为的新元素类型兼容。
几个例子应该能说明问题
public class Animal {} object [] data = new Animal[2]; // Animal [] is converted implicitly to object [] Animal [] animals1 = data; // Error: explicit conversion from object[] to Animal [] // is required Animal [] animals2 = (Animal[]) data; // object[] is converted explicit to Animal [] string [] strings1 = (string[]) animals2; // fails at compile time because // no conversion exists between two string [] strings2 = (string[]) data; // succeeds at compile because explict // conversion exists between object and string // but fails at runtime, because underlying // Animal array is not derived from string // array object [] data2 = new object[1]; data2[0] = new Animal(); Animal [] animals3 = (Animal[]) data2; // succeeds at compile time because explicit // conversion exists between object and Animal // fails at runtime even because data2's // underlying data type is object[] which is // not derived from Animal[] even though all // the elements of data2 are currently // Animals, the cast still because data2[], // being an array of objects, is not // constrained to Animals and // potentially data2[0] could be assigned // later a string or other type
能够将一种类型的数组重新解释为接口、基类型或派生类型的数组,从而在时间和内存方面实现效率,如果需要构造另一种类型的另一个数组,这些效率就会消失。
public void Test() { string data [] = new string [] { "a", "b", "c", "d", "e" }; SetRange(array, 1, 3, "x" ); } public void SetRange(object [] array, int start, int count, object value) { for (int i=0; i < count; i++) array[i+start] = value; }
在上面的例子中,新的字符串将显示为 { "a", "x", "x", "x", "e" }。尽管我们没有显式编写代码来处理字符串,但数组协变仍然允许 SetRange 对字符串数组起作用。将整数作为参数值将导致运行时异常,因为引用数组的所有数组赋值都包含运行时类型检查。
以下示例说明了在使用值类型数组和字符串数组与参数数组时出现的问题。调用带整数数组的 Write 会导致“System.Int32 []”被写出,因为整数数组被封装在另一个新构造的对象数组中。然而,调用带字符串数组的 Write 会导致“a”、“b”、“c”被写出;由于协变性,字符串数组成为 args 参数。
// This illustrates the difference in treatement between array and value types Write( new int [] { 1, 2, 3 } ); // Results in "System.Int32 []" being written Write( new string[] {"a", "b", "c" } ); // Results in "a", "b", "c" being written void Write(params object [] args) { for (int i=0; i<args.Length; i++) Console.WriteLine(args[i]); }
任何事物都有代价。数组协变的缺点是,每当一个元素被赋值一个新对象时,该对象都必须在运行时进行类型检查。
在使用协变编写通用数组函数时,总是可以选择使用 object[]
和 Array。在许多情况下,Object[]
速度更快,因为它清楚地表明数组是 SZARRAY
,并且 IL 具有用于设置和获取元素的特殊指令。然而,Array 总是可以保存值类型数组和多维数组。
数组元素的转换 (Array.Copy)
在相同类型的数组之间复制元素时,array.Copy
在传输前进行一次范围检查,然后进行超快速的 memmove 字节传输。
除了在不同类型数组之间复制元素外,array.Copy
还可以在不同类型之间复制元素。当从引用数组复制元素到值类型数组时,会执行拆箱;反之,则执行装箱。在不同值类型的数组之间,只执行扩展转换(例如,从 int 到 long,但不能从 long 到 int)。当无法执行转换时,会抛出 InvalidCastException
。在引用类型之间,会进行元素类型兼容性检查,并执行浅拷贝;对于不兼容的数组,会发生 ArrayTypeMismatchException
。
public Array Convert(Array array, Type type) { Array newArray = Array.CreateInstance(type, array.Length); Array.Copy(array,0, newArray,0, array.Length); return newArray; }
ArrayList 视图
ArrayList 的一些多功能性在于它能够构造其他 Array、ArrayList 和 IList 的各种类型的视图。
适配器
ArrayList.Adapter 允许将任何 IList(包括数组和许多其他集合)视为 ArrayList。这在几个方面都很有用:IList 可以使用 ArrayList 自动提供的二进制搜索、排序、反转、子范围和数组转换功能。然而,对于数组来说,这可能不那么有用,因为它已经提供了除了子范围视图之外的所有这些功能。
语法 | |
将 IList 转换为数组 | ArrayList.Adapter(iList).ToArray() |
反转 IList | ArrayList.Adapter(iList).Reverse() |
获取子范围 | ArrayList.Adapter(iList).GetRange(start, count) |
二进制搜索 | ArrayList.Adapter(iList).BinarySearch() |
Sort | ArrayList.Adapter(iList).Sort() |
数组子范围
要提取数组的子范围,可以编写以下代码。
public static Array GetRange(Array range, int start, int count) { Type type = array.Type; Array newArray = Array.CreateInstance(type.GetElementType(), count); Array.Copy(array, start, newArray, 0, count); return newArray; }
由于这会产生数组子范围的实际(浅层)副本,因此可能会消耗大量内存。ArrayList 类包含一个 GetRange(int start, int count) 方法,对于大型数组,它可能是节省内存的方法。GetRange 返回一个派生 ArrayList,它提供数组的视图。该视图也可以进行操作。可以从视图内部修改、添加和删除元素;但是,如果从视图外部对 arraylist 进行任何修改,则下次使用视图时将抛出异常。
此方法不创建元素的副本。新的 ArrayList 只是源 ArrayList 的一个视图窗口。但是,对源 ArrayList 的所有后续更改都必须通过此视图窗口 ArrayList 进行。如果直接对源 ArrayList 进行更改,则视图窗口 ArrayList 将失效,并且对其进行的任何操作都将返回 InvalidOperationException。
结合 Adapter 方法,也可以获得数组或其他集合的视图。例如,ArrayList.Adapter(array).GetRange(start, count)
提供了底层数组的视图。
包装支持
ArrayList 包含三个静态方法,每个方法都有两个重载,它们接受 IList 或 ArrayList,并分别返回固定大小、同步或只读的 IList 和 ArrayList。IList 方法返回一个继承自 IList 的轻量级包装类,由一个实例变量组成,该变量是对基 IList 的引用。ArrayList 方法返回一个派生 ArrayList 类,它由一个额外的实例变量组成,该变量引用基 ArrayList 类并忽略继承的数组。从 IList 重载的方法返回的 ILists 不派生自 ArrayList,并且在 ArrayList 类中有些错位。将它们作为 IList 接口的静态成员会更合适。例如,这些是 FixedList 的实际实现。
public static IList FixedSize(IList list) { if (list==null) throw new ArgumentNullException("list"); return new FixedSizeList(list); } public static ArrayList FixedSize(ArrayList list) { if (list==null) throw new ArgumentNullException("list"); return new FixedSizeArrayList(list); }
描述 | |
FixedSize | 返回固定大小的 IList 或 ArrayList。所有更改数组大小的操作(如 Add 或 Remove)都会导致 NotSupportedException。 |
ReadOnly | 返回一个只读的 IList 或 ArrayList。所有修改数组任何部分的操作都会导致 NotSupportedException。 |
Synchronized | 返回一个同步的 IList 或 ArrayList |
这些操作可以组合起来创建一个同步固定大小数组或同步只读数组:ArrayList.Synchronized(ArrayList.ReadOnly(list))
。只读数组会自动成为固定大小。
只读对于禁止修改特别有用。数组虽然是固定大小的,但仍然总是通过引用传递且可变。在封送处理中,大于十个元素的数组会被固定而不是复制,因此它们可能会被修改。
数组性能
范围检查消除:使用 for(int i=0; i<a.Length; i++)
C# 编译器执行一种特殊的优化,可以提高遍历数组的性能。首先,比较以下三种遍历数组的方法。哪种最快?
1) 标准迭代
int hash = 0; for (int i=0; i< a.Length; i++) { hash += a[i]; }
2) 使用保存长度变量的迭代循环
int hash = 0; int length = a.length; for (int i=0; i< length; i++) { hash += a[i]; }
3) foreach 迭代
foreach (int i in a)
{
hash += i;
}
在当前版本的 JIT 编译器中,你可能会惊讶地发现第一个示例生成的代码最快,而第三个“foreach”示例生成的代码最慢。在后续版本的编译器中,foreach
将针对数组进行特殊处理,以提供与示例 1 相同或更好的性能。
为什么示例 1 比示例 2 快?这是因为编译器识别了数组的 `for (int i=0; i<a.length; i++)` 模式。由于数组的长度是常量,编译器只是存储了长度,这样每次迭代都不会进行函数调用。(JIT 编译器实际上可能会内联对数组长度的引用,因为当前版本会自动内联由简单控制流和 < 32 字节 IL 指令组成的非虚方法调用。)
此外,编译器在循环内消除了任何 `s[i]` 实例上的所有范围检查测试,因为在 for 条件中 i 保证在 0 <= i < length 的范围内。通常,对数组的任何索引都会导致执行范围检查;这就是为什么在示例 2 中尝试通过存储长度变量来节省时间实际上导致代码比示例 1 慢的原因。
还有指针方法。
fixed (int *pfixed = a) { for (int *p = pfixed; count-->0; p++) hash += *p++; }
大数组
大数组可能会对性能产生显著影响。任何消耗 85K 的大对象都会被放置在大型对象堆中。实际上,几乎所有这些对象都将是数组,可能还有一些字符串,因为很少有类会包含足够的字段来超出该内存量。大型对象不会被压缩,并且只在包含第 2 代的完整垃圾回收中才会被移除。如果大型对象包含任何终结器,则至少需要两次完整垃圾回收。由于完整垃圾回收的频率可能比部分垃圾回收低 100 倍或更多,因此恢复内存可能需要很长时间。
因此,对于 .NET 应用程序来说,一个非常糟糕的分配方案,可能是最糟糕的,是那些频繁为临时用途分配大量内存的方案。
数组初始化
基本数据类型的静态数组在编译时初始化,就像 C++ 中一样。不幸的是,结构体静态数组不是在运行时初始化。开发人员推迟了此功能,因为结构体可以包含对象引用,这增加了复杂性。
预设 ArrayLists 和其他集合的大小
通常,.NET Framework 中的集合通过在每次填满时将其容量翻倍来动态调整大小。这是一种线性时间方法,需要 O(3N),优于将固定大小附加到数组的二次时间方法。但是,如果您事先知道最终数组的大致大小,则只需预分配即可将创建集合的时间缩短三分之二;集合将只进行 1N 次复制,而不是 3N 次复制。每个集合都有一个容量设置来实现这一点。
在多维数组上使用交错数组
在 Everett 之后的 C# 未来版本中,多维数组的许多性能问题都将得到解决。目前,依赖优化的 SZARRAYs 并且只需要稍多内存的交错数组要快得多。
尽可能使用强类型数组
强类型数组经过高度优化,避免了装箱、强制转换、函数调用和其他间接寻址的开销。对于值类型数组,许多函数(如 Reverse、IndexOf、LastIndexOf、BinSearch 和 Sort)
未来,"Visual Studio for Yukon" 中的 C# 有望支持泛型和约束,这将彻底消除装箱并允许函数尽可能高效地执行。更多信息可从微软的公开声明(网址:www.csharp.net)获取。
public class Stack<ItemType> { private ItemType[] items; public void Push(ItemType data) { ... } public ItemType Pop() { ... } }
泛型相对于 C# 模板有几个优点,例如消除了代码膨胀。专用类的代码在运行时动态生成,并且所有引用类型共享相同的代码。
结论
我的数组论述到此结束。我将在未来继续更新本文,提供新的源代码和实际基准测试。请务必关注此页面的更新版本。
由于本文引起的热情,我将继续创作更多“未公开”系列文章。我的下一篇文章将讨论数组和集合的实现。我希望在本系列完成后发表几十篇文章。我最终可能会将其整理成一本书。
我的资料来源包括各种书籍、共享源 CLI、MSDN、杂志文章、访谈、内部消息、开发者大会演示和一些反编译器。有读者建议我查阅 Don Box & Chris Sells 的《.NET Essentials》和 Jeffrey Richter 的《Applied .NET Framework》,它们提供了一些与我相同的信息;但我一直试图提出这两本书中都没有的新信息。
所有这些幕后信息都需要付出一定的研究和获取工作,所以,如果您喜欢这篇文章,别忘了投票。
版本历史
版本 | 描述 |
1月5日 | 关于数组的原始文章 |