第 16 章 数组
第 16 章 数组
NyxX第 16 章 数组
本章内容:
- 初始化数组元素
- 数组转型
- 所有数组都隐式派生自
System.Array - 所有数组都隐式实现
IEnumerable、ICollection和IList - 数组的传递和返回
- 创建下限非零的数组
- 数组的内部工作原理
- 不安全的数组访问和固定大小的数组
数组是允许将多个数据项作为集合来处理的机制。CLR 支持一维、多维和交错数组(即数组构成的数组)。所有数组类型都隐式地从 System.Array 抽象类派生,后者又派生自 System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚地说明了这一点:
1 | Int32[] myIntegers; // 声明一个数组引用 |
第一行代码声明 myIntegers 变量,它能指向包含 Int32 值的一维数组。myIntegers 刚开始设为 null,因为当时还没有分配数组。第二行代码分配了含有 100 个 Int32 值的数组,所有 Int32 都被初始化为 0。由于数组是引用类型,所以会在托管堆上分配容纳 100 个未装箱Int32所需的内存块。实际上,除了数组元素,数组对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员。该数组的内存块地址被返回并保存到myIntegers变量中。
CLS要求,所有数组都必须是 0 基数组(即最小索引为 0)。
每个数组都关联了一些额外的开销信息:
- 括数组的秩,即数组的维数
- 数组每一维的下限(几乎总是 0)
- 每一维的长度
- 数组的元素类型
多维数组
1 | // 创建一个二维数组,由 Double 值构成 |
交错数组
1 | // 创建由多个 Point 数组构成的一维数组 |
16.1 初始化数组元素
前面展示了如何创建数组对象,如何初始化数组中的元素。C# 允许用一个语句做这两件事情。例如:
String[] names = new String[] { "Aidan", "Grant" };
大括号中的以逗号分隔的数据的数据项称为数组初始化器(array initializer)。每个数据项都可以是一个任意复杂度的表达式;在多维数组的情况下,则可以是一个嵌套的数组初始化器。
可利用 C# 的“隐式类型的局部变量”功能来简化一下代码:
1 | // 利用 C# 的隐式类型的局部变量功能: |
1 | // 利用 C#的隐式类型的局部变量和隐式类型的数组功能: |
作为初始化数组时的一个额外的语法奖励,还可以像下面这样写:
String[] names = { "Aidan", "Grant" };
C#编译器不允许在这种语法中使用隐式类型的局部变量:
1 | // 试图使用隐式类型的局部变量(错误) |
1 | // 使用 C# 的隐式类型的局部变量、隐式类型的数组和匿名类型功能: |
16.2 数组转型
对于元素为引用类型的数组,CLR 允许将数组元素从一种类型转型另一种。成功转型要求数组维数相同,而且必须存在从元素源类型到目标类型的隐式或显式转换。CLR 不允许将值类型元素的数组转型为其他任何类型。
1 | // 创建二维 FileStream 数组 |
Array.Copy 的作用不仅仅是将元素从一个数组复制到另一个。Copy方法还能正确处理内存的重叠区域,就像 C 的 memmove 函数一样。有趣的是, C 的 memcpy 函数反而不能正确处理处理重叠的内存区域。Copy方法还能在复制每个数组元素时进行必要的类型转换,具体如下所述:
将值类型的元素装箱为引用类型的元素,比如将一个
Int32[]复制到一个Object[]中。将引用类型的元素拆箱为值类型的元素,比如将一个
Object[]复制到一个Int32[]中。加宽 CLR 基元值类型,比如将一个
Int32[]的元素复制到一个Double[]中。在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性,比如从
Object[]转型为IFormattable[],就根据需要对元素进行向下类型转换。如果Object[]中的每个对象都实现了IFormattable,Copy方法就能成功执行。
下面演示了Copy方法的另一种用法:
1 | // 定义实现了一个接口的值类型 |
16.3 所有数组都隐式派生自 System.Array
System.Array定义了许多有用的实例方法和属性,比如 Clone,CopyTo,GetLength,GetLongLength,GetLowerBound,GetUpperBound,Length,Rank等。
System.Array类型还公开了很多有用的、用于数组处理的静态方法。这些方法均获取一个数组引用作用作为参数。一些有用的静态方法包括:AsReadOnly,BinarySearch,Clear,ConstrainedCopy,ConvertAll,Copy,Exists,Find,FindAll,FindIndex,FindLast,FindLastIndex,Foreach,IndexOf,LastIndexOf,Resize,Sort和TrueForAll。这些方法中,每个都有多个重载版本,能保障编译时的类型安全性和良好的性能。
16.4 所有数组都隐式实现 IEnumerable,ICollection和IList
创建一维 0 基数组类型时,CLR 自动使数组类型实现IEnumerable<T>,ICollection<T>和IList<T>(T是数组元素的类型)。同时,还为数组类型实现这三个接口,只要它们是引用类型。
所以,如果执行以下代码:
FileStream[] fsArray;
那么当 CLR 创建FileStream[]类型时,会自动为这个类型实现 IEnumerable<FileStream>,ICollection<FileStream>和IList<FileStream>接口。此外,FileStream[]类型还会为基类型实现接口:IEnumerable<Stream>,IEnumerable<Object>,ICollection<Stream>,ICollection<Object>,IList<Stream>和IList<Object>。由于所有这些接口都由 CLR 自动实现,所以在存在这些接口任何地方都可以使用 fsArray 变量。例如,可将 fsArray 变量传给具有以下任何一种原型的方法:
1 | void M1(IList<FileStream> fsList) { ... } |
注意,如果数组包含值类型的元素,数组类型不会为元素的基类型实现接口。例如,如果执行以下代码:
DateTime[] dtArray; // 一个值类型的数组
那么 DateTime[] 类型只会实现 IEnumerable<DateTime>,ICollection<DateTime>和IList<DateTime>接口,不会为DateTime的基类型(包括System.ValueType和System.Object)实现这些泛型接口。这意味着dtArray变量不能作为实参传给前面的M3方法。这是因为值类型的数组在内存中的布局与引用类型的数组不同。数组内存的布局请参见本章前面的描述。
16.5 数组的传递和返回
当将数组作为实参传递给方法时,实际传递的是对该数组的引用。这意味着被调用的方法可以修改数组中的元素。如果不想被修改,需要生成数组的拷贝并将拷贝传递给方法。
另外,如果方法返回对数组的引用,可以选择返回原始数组的引用或构造一个新数组并返回该新数组的引用。如果返回一个新数组的引用,应该使用Array.Copy方法来进行浅拷贝。
如果定义返回数组引用的方法且数组中没有元素,建议返回对一个包含零个元素的数组的引用,因为这样可以简化代码。
16.7 数组的内部工作原理
CLR 内部支持两种不同的数组:
- 下限为0的一维数组,也称为SZ数组或向量。
- 下限未知的一维或多维数组。
访问一维0基数组的元素比访问非0基一维或多维数组的元素稍快。原因有两个:一是一维0基数组有特殊的IL指令,可以优化JIT编译器生成的代码;二是JIT编译器能将索引范围检查代码从循环中拿出,只执行一次。而访问非0基一维数组或多维数组的速度相对较慢,因为需要进行索引检查和减去数组下限。为了提高性能,可以考虑使用交错数组代替矩形数组。
C# 和 CLR 还允许使用 unsafe(不可验证)代码访问数组。这种技术实际能在访问数组时关闭索引上下限检查。这种不安全的数组访问技术适合以下元素类型的数组:SByte、Byte、Int16、UInt16、Int32、UInt32、Int64、UInt64、Char、Single、Double、Decimal、Boolean、枚举类型或者字段为上述任何类型的值类型结构。
这个功能很强大,但使用须谨慎,因为它允许直接内存访问。访问越界(超出数组上下限)不会抛出异常,但会损失内存中的数据,破坏类型安全性,并可能造成安全漏洞!
以下 C# 代码演示了访问二维数组的三种方式(安全、交错和不安全):
1 | using System; |
16.8 不安全的数组访问和固定大小的数组
不安全的数组访问非常强大,因为它允许访问以下元素。
- 堆上的托管数组对象中的元素(上一节对此进行了演示)。
- 非托管堆上的数组中的元素。
- 线程栈上的数组中的元素。
如果性能是首要目标,请避免在堆上分配托管的数组对象。相反,应该使用C#的stackalloc语句在线程栈上分配数组。
stackalloc语句只能创建一维0基、由值类型元素构成的数组,并且不能包含引用类型的字段。它实际上分配了一个内存块,可以使用不安全的指针来操作。
栈上分配的内存(数组)会在方法返回时自动释放,对性能有一定的提升。使用这个功能需要为C#编译器指定/unsafe开关。
在结构中嵌入数组需满足以下几个条件:
通常,由于数组是引用类型,所以结构中定义的数组字段实际只是指向数组的指针或引用;数组本身在结构的外部。不过,也可像上述代码中的CharArray结构那样,直接将数组嵌入结构。在结构中嵌入数组需满足以下几个条件。
类型必须是结构(值类型);不能在类(引用类型)中嵌入数组。
字段或其定义结构必须用
unsafe关键字标记。数组字段必须用
fixed关键字标记。数组必须是一维 0 基数组。
数组的元素类型必须是以下类型之一:
Boolean,Char,SByte,Byte,Int16,Int32,UInt16,UInt32,Int64,Single或Double。





