17 个数组
17.1 常规
数组是一种数据结构,其中包含许多通过计算索引访问的变量。 数组中的变量(亦称为数组的元素)均为同一种类型,我们将这种类型称为数组的元素类型。
数组具有确定与每个数组元素关联的索引数的排名。 数组的排名也称为数组的维度。 排名为一的数组称为 单维数组。 排名大于一的数组称为 多维数组。 特定大小的多维数组通常称为二维数组、三维数组等。 数组的每个维度都有一个关联的长度,该长度是大于或等于零的整数。 维度长度不是数组类型的一部分,而是在运行时创建数组类型的实例时建立的。 维度的长度决定了该维度的有效索引范围:对于长度N
的维度,索引的范围可以是非0
N – 1
独占的。 数组中的元素总数是数组中每个维度长度的乘积。 如果数组的一个或多个维度的长度为零,则表示该数组为空。
数组的元素类型本身可以是数组类型(§17.2.1)。 数组的此类数组不同于多维数组,可用于表示“交错数组”。
示例:
int[][] pascals = { new int[] {1}, new int[] {1, 1}, new int[] {1, 2, 1}, new int[] {1, 3, 3, 1} };
end 示例
每个数组类型都是引用类型(§8.2)。 数组的元素类型可以是任何类型的,包括值类型和数组类型。
17.2 数组类型
17.2.1 常规
数组类型的语法生产在 §8.2.1 中提供。
数组类型以non_array_type形式写入,后接一个或多个rank_specifier。
non_array_type是类型本身不是array_type。
数组类型的排名由array_type中最左侧的rank_specifier提供:rank_specifier指示数组是一个数组,其排名为一个加上rank_specifier中的“,
”标记数。
数组类型的元素类型是删除最 左侧rank_specifier的结果类型:
- 窗体
T[R]
的数组类型是具有排名R
和非数组元素类型的T
数组。 - 窗体
T[R][R₁]...[Rₓ]
的数组类型是具有排名R
和元素类型的T[R₁]...[Rₓ]
数组。
实际上,rank_specifier在最终的非数组元素类型之前从左到右读取。
示例:类型
T[][,,][,]
为二维数组的三维数组的一维数组int
。 end 示例
在运行时,数组类型的值可以是 null
或对该数组类型的实例的引用。
注意:遵循 §17.6 的规则,该值也可能是对协变数组类型的引用。 end note
17.2.2 System.Array 类型
该类型 System.Array
是所有数组类型的抽象基类型。 隐式引用转换(§10.2.8)从任何数组类型到 System.Array
实现的任何接口类型 System.Array
存在。 显式引用转换(§10.3.5)存在于 System.Array
由任何数组类型实现 System.Array
的任何接口类型。 System.Array
本身不是 array_type。 相反,这是一个 class_type ,从中派生所有 array_type。
在运行时,类型 System.Array
值可以是 null
或引用任何数组类型的实例。
17.2.3 数组和泛型集合接口
单维数组 T[]
实现接口 System.Collections.Generic.IList<T>
(IList<T>
短)及其基接口。 因此,从其基接口进行隐式转换T[]
IList<T>
。 此外,如果有从到然后实现的隐式引用转换S
T
,并且有从其IList<T>
基接口的隐式引用转换S[]
(§10.2.8)。IList<T>
S[]
如果从显式引用转换S
到,则从其基接口(§10.3.5)进行显式引用转换S[]
IList<T>
。T
同样,单维数组 T[]
还实现接口 System.Collections.Generic.IReadOnlyList<T>
(IReadOnlyList<T>
短)及其基接口。 因此,从其基接口进行隐式转换T[]
IReadOnlyList<T>
。 此外,如果有从到然后实现的隐式引用转换S
T
,并且有从其IReadOnlyList<T>
基接口的隐式引用转换S[]
(§10.2.8)。IReadOnlyList<T>
S[]
如果从显式引用转换S
到,则从其基接口(§10.3.5)进行显式引用转换S[]
IReadOnlyList<T>
。T
示例:例如:
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 IReadOnlyList<string> lst7 = sa; // Ok IReadOnlyList<string> lst8 = oa1; // Error, cast needed IReadOnlyList<object> lst9 = sa; // Ok IReadOnlyList<object> lst10 = oa1; // Ok IReadOnlyList<string> lst11 = (IReadOnlyList<string>)oa1; // Exception IReadOnlyList<string> lst12 = (IReadOnlyList<string>)oa2; // Ok } }
分配
lst2 = oa1
生成编译时错误,因为从转换object[]
到IList<string>
的转换是显式转换,而不是隐式转换。 强制转换(IList<string>)oa1
将导致在运行时引发异常,因为oa1
引用了非object[]
string[]
引用 。 但是,强制转换(IList<string>)oa2
不会引发异常,因为oa2
引用了一个string[]
。end 示例
每当有隐式或显式引用转换S[]
到时IList<T>
,也存在从其基接口到 S[]
(§10.3.5) 的显式引用转换IList<T>
。
当数组类型 S[]
实现时,已实现 IList<T>
接口的某些成员可能会引发异常。 接口实现的精确行为超出了此规范的范围。
17.3 数组创建
数组实例由array_creation_expression s(§12.8.16.5)或包含array_initializer(§17.7)的字段或局部变量声明创建。 还可以隐式创建数组实例,作为计算涉及参数数组的参数列表的一部分(§15.6.2.4)。
创建数组实例时,将建立每个维度的排名和长度,然后在实例的整个生存期内保持常量。 换句话说,无法更改现有数组实例的排名,也不能调整其维度的大小。
数组实例始终是数组类型。 该 System.Array
类型是无法实例化的抽象类型。
array_creation_expression创建的数组元素始终初始化为其默认值(§9.3)。
17.4 Array 元素访问
数组元素是使用表单的element_access表达式(§12.8.11.2)访问的,其中A
数组类型的表达式,每个Iₑ
表达式是类型int
、uint
类型、long
或ulong
可隐式转换为其中一A[I₁, I₂, ..., Iₓ]
个或多个类型的表达式。 数组元素访问的结果是一个变量,即索引选择的数组元素。
可以使用语句(§13.9.5)枚举foreach
数组的元素。
17.5 数组成员
每个数组类型都继承该 System.Array
类型声明的成员。
17.6 数组协变
对于任意两个reference_type,A
并且B
,如果隐式引用转换(§10.2.8)或显式引用转换(§10.3.5)存在,B
则数组类型A[R]
也存在A
相同的引用转换,B[R]
其中R
任何给定rank_specifier(但两种数组类型的引用都相同)。 此关系称为 数组协变。 数组协变(特别是)表示,数组类型的值实际上可能是对数组类型的A[R]
B[R]
实例的引用,前提是存在B
从 到A
的隐式引用转换。
由于数组协变,对引用类型数组元素的赋值包括运行时检查,以确保分配给数组元素的值实际上是允许的类型(§12.21.2)。
示例:
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
中,但第三个调用会导致在执行第一个System.ArrayTypeMismatchException
赋值array[i]
时引发。 发生异常的原因是无法将装箱int
存储在数组中string
。end 示例
数组协变专门不扩展到 value_type数组。 例如,不存在允许将转换 int[]
视为一个 object[]
转换。
17.7 数组初始值设定项
可以在字段声明(§15.5)、局部变量声明(§13.6.2)和数组创建表达式(§12.8.16.5)中指定数组初始值设定项:
array_initializer
: '{' variable_initializer_list? '}'
| '{' variable_initializer_list ',' '}'
;
variable_initializer_list
: variable_initializer (',' variable_initializer)*
;
variable_initializer
: expression
| array_initializer
;
数组初始值设定项由一系列变量初始值设定项组成,由“”{
和“}
”标记括起来,并用“,
”标记分隔。 每个变量初始值设定项都是表达式,或者,对于多维数组,是嵌套数组初始值设定项。
在其中使用数组初始值设定项的上下文决定了要初始化的数组的类型。 在数组创建表达式中,数组类型紧邻初始值设定项之前,或从数组初始值设定项中的表达式推断。 在字段或变量声明中,数组类型是声明的字段或变量的类型。 在字段或变量声明中使用数组初始值设定项时,
int[] a = {0, 2, 4, 6, 8};
它只是等效数组创建表达式的简写:
int[] a = new int[] {0, 2, 4, 6, 8};
对于单维数组,数组初始值设定项应包含一系列表达式,每个表达式都具有对数组的元素类型的隐式转换(§10.2)。 表达式以递增顺序初始化数组元素,从索引为零的元素开始。 数组初始值设定项中的表达式数决定了要创建的数组实例的长度。
示例:上面的数组初始值设定项创建
int[]
长度为 5 的实例,然后使用以下值初始化实例:a[0] = 0; a[1] = 2; a[2] = 4; a[3] = 6; a[4] = 8;
end 示例
对于多维数组,数组初始值设定项的嵌套级别应与数组中存在维度一样多。 最外部的嵌套级别对应于最左侧的维度,最内部的嵌套级别对应于最右侧的维度。 数组的每个维度的长度取决于数组初始值设定项中相应嵌套级别的元素数。 对于每个嵌套数组初始值设定项,元素数应与同一级别的其他数组初始值设定项相同。
示例:示例:
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;
end 示例
如果除最右侧以外的维度具有长度为零,则假定后续维度的长度也为零。
示例:
int[,] c = {};
为最左侧和最右侧的维度创建长度为零的二维数组:
int[,] c = new int[0, 0];
end 示例
当数组创建表达式包括显式维度长度和数组初始值设定项时,长度应为常量表达式,每个嵌套级别的元素数应与相应的维度长度匹配。
示例:下面是一些示例:
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
项中的长度和元素数不同意。end 示例
注意:C# 允许在array_initializer末尾使用尾随逗号。 此语法提供了从此类列表中添加或删除成员的灵活性,并简化了此类列表的计算机生成。 end note