C# 语言规范_版本5.0 (第12章 数组)
1. 数组
数组是一种包含若干变量的数据结构,这些变量都可以通过计算索引进行访问。数组中包含的变量(又称数组的元素)具有相同的类型,该类型称为数组的元素类型。
数组有一个“秩”,它确定和每个数组元素关联的索引个数。数组的秩又称为数组的维度。“秩”为 1 的数组称为一维数组 (single-dimensional array)。“秩”大于 1 的数组称为多维数组 (multi-dimensional array)。维度大小确定的多维数组通常称为两维数组、三维数组等。
数组的每个维度都有一个关联的长度,它是一个大于或等于零的整数。维度的长度不是数组类型的组成部分,而只与数组类型的实例相关联,它是在运行时创建实例时确定的。维度长度确定该维度索引的有效范围:如果维度长度为 N,则索引的范围可以从 0 到 N – 1(包括 N – 1)。数组中的元素总数是数组中各维度长度的乘积。如果数组的一个或多个维度的长度为零,则称该数组为空。
数组的元素类型可以是任意类型,包括数组类型。
1.1 数组类型
数组类型表示为一个 non-array-type 后接一个或多个 rank-specifier:
array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[
dim-separatorsopt ]
dim-separators:
,
dim-separators ,
non-array-type 是本身不是 array-type 的任意 type。
由 array-type 中最左侧的 rank-specifier 给定数组类型的秩:rank-specifier 表示该数组是其秩为 1 加上 rank-specifier 中的“,”标记个数的数组。
数组类型的元素类型就是去掉最左边的 rank-specifier 后剩余表达式的类型:
- 形式为 T[R] 的数组类型是秩为 R、元素类型为非数组元素类型 T 的数组。
- 形式为 T[R][R1]...[RN] 的数组类型是秩为 R、元素类型为 T[R1]...[RN] 的数组。
实质上,在解释数组类型时,先从左到右读取 rank-specifier,最后才读取那个最终的非数组元素类型。例如,类型 int[][,,][,] 表示一个一维数组,该一维数组的元素类型为三维数组,该三维数组的元素类型为二维数组,该二维数组的元素类型为 int。
在运行时,数组类型的值可以为 null 或对该数组类型的某个实例的引用。
1.1.1 System.Array 类型
System.Array 类型是所有数组类型的抽象基类型。存在从任何数组类型到 System.Array 的隐式引用转换(第 6.1.6 节),并且存在从 System.Array 到任何数组类型的显式引用转换(第 6.2.4) 节)。请注意,System.Array 本身不是 array-type。相反,它是一个从中派生所有 array-type 的 class-type。
在运行时,System.Array 类型的值可以是 null 或是对任何数组类型的实例的引用。
1.1.2 数组和泛型 IList 接口
一维数组 T[] 实现了接口 System.Collections.Generic.IList<T>(缩写为 IList<T>)及其基接口。相应地,存在从 T[] 到 IList<T> 及其基接口的隐式转换。此外,如果存在从 S 到 T 的隐式引用转换,则 S[] 实现 IList<T>,并且存在从 S[] 到 IList<T> 及其基接口的隐式引用转换(第 6.1.6 节)。如果存在从 S 到 T 的显式引用转换,则存在从 S[] 到 IList<T> 及其基接口的显式引用转换(第 6.2.4 节)。例如:
using System.Collections.Generic;
class Test
{
static void Main() {
string[] sa = new string[5];
object[] oa1 = new object[5];
object[] oa2 = sa;
IList<string> lst1 = sa; // Ok
IList<string> lst2 = oa1; // Error, cast needed
IList<object> lst3 = sa; // Ok
IList<object> lst4 = oa1; // Ok
IList<string> lst5 = (IList<string>)oa1; // Exception
IList<string> lst6 =
(IList<string>)oa2; // Ok
}
}
赋值操作 lst2 = oa1 将产生编译时错误,因为从 object[] 到 IList<string> 的转换是显式转换,不是隐式转换。强制转换 (IList<string>)oa1 会导致在运行时引发异常,因为 oa1 引用 object[] 而不是 string[]。但是,强制转换 (IList<string>)oa2 不会导致在运行时引发异常,因为 oa2 引用 string[]。
如果存在从 S[] 到 IList<T> 的隐式或显式引用转换,则也存在从 IList<T> 及其基接口到 S[] 的显式引用转换(第 6.2.4 节)。
当数组类型 S[] 实现 IList<T> 时,所实现的接口的有些成员可能会引发异常。该接口的实现的确切行为不在本规范讨论的范围之内。
1.2 数组创建
数组实例是由 array-creation-expression(第 7.6.10.4 节)创建的,或者是由包含 array-initializer(第 12.6 节)的字段声明或局部变量声明创建的。
创建数组实例时,将确定秩和各维度的长度,它们在该实例的整个生存期内保持不变。换言之,对于一个已存在的数组实例,既不能更改它的秩,也不可能调整它的维度大小。
数组实例一定是数组类型。System.Array 类型是不能实例化的抽象类型。
由 array-creation-expression 创建的数组的元素总是被初始化为它们的默认值(第 5.2 节)。
1.3 数组元素访问
数组元素使用形式为 A[I1, I2, ..., IN] 的 element-access 表达式(第 7.6.6.1 节)进行访问,其中 A 是数组类型的表达式,每个 IX 都是 int、uint、long、ulong 类型的表达式,或者可以隐式转换为这些类型的一种或多种类型。数组元素访问的结果是变量,即由下标选定的数组元素。
此外,还可以使用 foreach 语句(第 8.8.4) 节)来枚举数组的各个元素。
1.4 数组成员
每个数组类型均继承由 System.Array 类型声明的成员。
1.5 数组协变
对于任意两个 reference-type A 和 B,如果存在从 A 到 B 的隐式引用转换(第 6.1.6 节)或显式引用转换(第 6.2.4 节),则也一定存在从数组类型 A[R] 到数组类型 B[R] 的相同的引用转换,其中 R 可以是任何给定的 rank-specifier,但这两个数组类型必须使用相同的 R。这种关系称为数组协变。具体而言,数组协变意味着数组类型 A[R] 的值实际上可能是对数组类型 B[R] 的实例的引用(如果存在从 B 到 A 的隐式引用转换)。
由于存在数组协变,对引用类型数组的元素的赋值操作会包括一个运行时检查,以确保正在赋给数组元素的值确实是允许的类型(第 7.17.1 节)。例如:
class Test
{
static void Fill(object[] array, int
index, int count, object value) {
for (int i = index; i < index +
count; i++) array[i] = value;
}
static
void Main() {
string[] strings = new string[100];
Fill(strings, 0, 100,
"Undefined");
Fill(strings, 0, 10, null);
Fill(strings, 90, 10, 0);
}
}
Fill 方法中对 array[i] 的赋值隐式包括运行时检查,该检查可确保由 value 引用的对象是 null 或与 array 的实际元素类型兼容的实例。在 Main 中,Fill 的前两次调用会成功,但第三次调用在执行 array[i] 的第一个赋值操作时会引发 System.ArrayTypeMismatchException。发生此异常是因为装箱的 int 类型不能存储在 string 数组中。
具体而言,数组协变不能扩展至 value-type 的数组。例如,不存在允许将 int[] 当作 object[] 来处理的转换。
1.6 数组初始值设定项
数组初始值设定项可以在字段声明(第 10.5 节)、局部变量声明(第 8.5.1 节)和数组创建表达式(第 7.6.10.4 节)中指定:
array-initializer:
{
variable-initializer-listopt
}
{
variable-initializer-list , }
variable-initializer-list:
variable-initializer
variable-initializer-list , variable-initializer
variable-initializer:
expression
array-initializer
数组初始值设定项包含一系列变量初始值设定项,它们括在“{”和“}”标记中并且用“,”标记分隔。每个变量初始值设定项是一个表达式,或者(在多维数组的情况下)是一个嵌套的数组初始值设定项。
数组初始值设定项所在位置的上下文确定了正在被初始化的数组的类型。在数组创建表达式中,数组类型紧靠初始值设定项之前,或者由数组初始值设定项中的表达式推断得出。在字段或变量声明中,数组类型就是所声明的字段或变量的类型。当数组初始值设定项用在字段或变量声明中时,如:
int[] a = {0, 2, 4, 6, 8};
它只是下列等效数组创建表达式的简写形式:
int[] a = new int[] {0, 2, 4, 6, 8};
对于一维数组,数组初始值设定项必须包含一个表达式序列,这些表达式是与数组的元素类型兼容的赋值表达式。这些表达式从下标为零的元素开始,按照升序初始化数组元素。数组初始值设定项中所含的表达式的数目确定正在创建的数组实例的长度。例如,上面的数组初始值设定项创建了一个长度为 5 的 int[] 实例并用下列值初始化该实例:
a[0] = 0; a[1] = 2; a[2] = 4; a[3] = 6; a[4] = 8;
对于多维数组,数组初始值设定项必须具有与数组维数同样多的嵌套级别。最外面的嵌套级别对应于最左边的维度,而最里面的嵌套级别对应于最右边的维度。数组各维度的长度是由数组初始值设定项中相应嵌套级别内的元素数目确定的。对于每个嵌套的数组初始值设定项,元素的数目必须与同一级别的其他数组初始值设定项所包含的元素数相同。示例:
int[,] b = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}};
创建一个二维数组,其最左边的维度的长度为 5,最右边的维度的长度为 2:
int[,] b = new int[5, 2];
然后用下列值初始化该数组实例:
b[0, 0] = 0;
b[0, 1] = 1;
b[1, 0] = 2; b[1, 1] = 3;
b[2, 0] = 4; b[2, 1] = 5;
b[3, 0] = 6; b[3, 1] = 7;
b[4, 0] = 8; b[4, 1] = 9;
如果指定非最右边的维度的长度为零,则假定后续维度的长度也为零。示例:
int[,] c = {};
创建一个二维数组,其最左边和最右边的维度的长度均为零。
int[,] c = new int[0, 0];
当数组创建表达式同时包含显式维度长度和一个数组初始值设定项时,长度必须是常量表达式,并且各嵌套级别的元素数目必须与相应的维度长度匹配。以下是几个示例:
int i = 3;
int[] x = new int[3] {0, 1, 2}; // OK
int[] y = new int[i] {0, 1, 2}; //
Error, i not a constant
int[] z = new int[3] {0, 1, 2, 3}; //
Error, length/initializer mismatch
这里,由于维度长度表达式不是常量,因此 y 的初始值设定项导致编译时错误;另外由于初始值设定项中所设定的长度和元素数目不一致,z 的初始值设定项也导致编译时错误。