它将在CLR托管堆里分配一块连续的内存空间,足以容纳数据类型为arrayTypes、个数为allocationSize的数组元素。如果arrayType为值类型(译注:如int类型),则有allocationSize个未封箱(unboxed)的arrayType值被创建。如果arrayType为引用类型(译注:如string类型),则有allocationSize个arrayType引用类型值被创建。(如果你对值类型和引用类型、托管堆和栈之间的区别不熟悉,请查阅―理解.Net公共类型系统Common Type System‖) 为帮助理解.Net Framework中数组的内部存储机制,请看下面的例子: arrayName = new arrayType[allocationSize];
This allocates a contiguous block of memory in the CLR-managed heap large enough to hold the allocationSize number of arrayTypes. If arrayType is a value type, then allocationSize number of unboxed arrayType values are created. If arrayType is a reference type, then allocationSize number of arrayType references are created. (If you are unfamiliar with the difference between reference and value types and the managed heap versus the stack, check out Understanding .NET's Common Type System.)
To help hammer home how the .NET Framework stores the internals of an array, consider the following example: bool [] booleanArray; FileInfo [] files;
booleanArray = new bool[10]; files = new FileInfo[10];
这里,booleanArray是值类型System.Boolean数组,而files数组则是引用类型System.IO.FileInfo数组。图一显示了执行这四行代码后CLR托管堆的情况。
图一:在托管堆中顺序存放数组元素请记住在files数组中存放的十个元素指向的是FileInfo实例。图二强调了这一点(hammers home this point,有些俚语的感觉,不知道怎么翻译),
显示了如果我们为files数组中的FileInfo实例分配一些值后内存的分布情况。
图二:在托管堆中顺序存放数组元素
.Net中所有数组都支持对元素的读写操作。访问数组元素的语法格式如下: // 读一个数组元素
bool b = booleanArray[7]; // 写一个数组元素,即赋值 booleanArray[0] = false;
访问一个数组元素的运行时间表示为O(1),因为对它的访问时间是不变的。那就是说,不管数组存储了多少元素,查找一个元素所花的时间都是相同的。运行时间之所以不变,是因为数组元素是连续存放的,查找定位的时候只需要知道数组在内存中的起始位置,每个元素的大小,以及元素的索引值。
在托管代码中,数组的查找比实际的实现稍微复杂一些,因为在CLR中访问每个数组,都要确保索引值在其边界之内。如果数组索引超出边界,会抛出IndexOutOfRangeException异常。这种边界检查有助于确保我们在访问数组不至于意外地超出数组边界而进入另外一块内存区。而且它不会影响数组访问的时间,因为执行边界检查所需时间并不随数组元素的增加而增加。 注:如果数组元素特别多,索引边界检查会对应用程序的执行性能有稍许影响。而对于非托管代码,这种边界检查就被忽略了。要了解更多信息,请参考Jeffrey Richter所著的Applied Microsoft .NET Framework Programming第14章。
使用数组时,你也许需要改变数组大小。可以通过根据特定的长度大小创建一个新数组实例,并将旧数组的内容拷贝到新数组,来实现该操作。我们称这一过程为数组空间重分配(redimensioning),如下代码: using System;
using System.Collections; public class MyClass {
public static void Main() {
// 创建包含3个元素的int类型数组 int [] fib = new int[3]; fib[0] = 1; fib[1] = 1; fib[2] = 2;
// 重新分配数组,长度为10 int [] temp = new int[10]; // 将fib数组内容拷贝到临时数组 fib.CopyTo(temp, 0);
// 将临时数组赋给fib fib = temp; } }
在代码的最后一行,fib指向包含10个元素的Int32类型数组。Fib数组中3到9(译注:注意下标从0开始)的元素值默认为0(Int32类型)。
当我们要存储同种类型的数据(原文为heterogeneous types——异类数据类型,我怀疑有误)并仅需要直接访问数据时,数组是较好的数据结构。搜索未排序的数组时间复杂度是线形的。当
我们对小型数组进行操作,或很少对它进行查询操作时,数组这种结构是可以接受的。但当你的应用程序需要存储大量数据,且频繁进行查询操作时,有很多其他数据结构更能适应你的工作。我们来看看本文接下来将要介绍的一些数据结构。(如果你要根据某个属性查找数组,且数组是根据该属性进行排序的,你可以使用二叉法(binary search)对其搜索,它的时间复杂度为O(log n),与在二叉树中搜索的时间复杂度相同。事实上,数组类中包含了一个静态方法BinarySearch()。如要了解该方法的更多信息,请参考我早期的一篇文章―有效地搜索有序数组‖。
注:.Net Framework同样支持多维数组。与一维数组一样,多维数组对数据元素的访问运行时间仍然是不变的。回想一下我们前面介绍的在n个元素的一维数组中查询操作的时间复杂度为O(n)。对于一个nxn的二维数组,时间复杂度为O(n2),因为每次搜索都要检查n2个元素。以此类推,k维数组搜索的时间复杂度为O(nk)。 ArrayList:可存储不同类型数据、自增长的数组
明确地,数组在设计时受到一些限制,因为一维数组只能存储相同类型的数据,而且在使用数组时,必须为数组定义特定的长度。很多时候,开发人员要求数组更加灵活,它可以存储不同类型的数据,也不用去关心数组空间的分配。在.Net Framework基类库中提供了满足这样条件的数据结构——System.Collections.ArrayList。
如下的一小段代码是ArrayList的示例。注意到使用ArrayList时可以添加任意类型的数据,且不需要分配空间。所有的这些都由系统控制。 ArrayList countDown = new ArrayList(); countDown.Add(5); countDown.Add(4); countDown.Add(3); countDown.Add(2); countDown.Add(1);
countDown.Add(\ countDown.Add(new ArrayList());
从深层次的含义来讲,ArrayList使用的存放类型为object的System.Array对象。既然所有类型都是直接或间接从object派生,自然一个object类型的数组也可以存放任何类型的元素。
ArrayList默认创建16个object类型元素的数组,当然我们也可以通过构造函数中的参数或设置Capacity属性来定制ArrayList大小。通过Add()方法添加新元素,数组内部自动检查其容量。如果添加新元素导致越界,则容量则自动成倍增加,我们称为自增长。 ArrayList和Array一样,也可以通过索引直接访问: // Read access
int x = (int) countDown[0]; string y = (string) countDown[5]; // Write access countDown[1] = 5;
// 会产生ArgumentOutOfRange 异常 countDown[7] = 5;
既然ArrayList存储的是object类型的元素,因此从ArrayList中读元素时应该显示的指定类型转换。同时要注意的是,如果你访问的数组元素超过ArrayList的长度,系统会抛出System.ArgumentOutOfRange异常。
ArrayList提供了标准数组所不具备的自增长灵活性,但这种灵活性是以牺牲性能为代价的,尤其是当我们存储的是值类型——例如System.Int32,System.Double,System.Boolean等。它们在托管堆中是以未封箱形式(unboxed form)连续存放的。然而,ArrayList的内部机制是一个引用的object对象数组;因此,即使ArrayList中只存放了值类型,这些元素仍然会通过封箱(boxing)转换为引用类型。如图三所示: