数组
数组
数组实际上是由一个变量名称表示的一组同类型的数据元素。每个元素通过变量名称和一个或多个方括号中的索引来访问:
1数组名 索引 2 ↓ ↓ 3MyArray[4]
定义
让我们从C#中与数组有关的重要定义开始
- 元素 数组的独立数据项称为元素。数组的所有元素必须是同类型的,或继承自相同的类型
- 秩/维度 数组可以有任何为正数的维度数。数组的维度数称作秩(rank)
- 维度长度 数组的每个维度都有一个长度,就是这个方向的位置个数
- 数组长度 数组的所有维度中的元素的总和称为数组的长度
重要细节
关于C#数组的一些要点:
- 数组一旦创建,大小就固定了。C#不支持动态数组
- 数组索引号从0开始。如果维度长度是n,索引号范围就是0到n-1

数组的类型
C#提供两种类型的数组
- 一维数组可以认为是单行元素或元素向量
- 多维数组是由主向量中的位置组成的。每个位置本身又是一个数组,称为子数组(subarray)。子数组向量中的位置本身又是一个子数组
另外,有两种类型的多维度数组:矩形数组(rectangular array)和交错数组(jagged array),它们有如下特性:
- 矩形数组
- 某个维度的所有子数组具有相同长度的多维数组
- 不管有多少个维度,总是使用一组方括号
int x=myArray2[4,6,1]
- 交错数组
- 每个子数组都是独立数组的多维度数组
- 可以有不同长度的子数组
- 为数组的每个维度使用一对方括号
jagArray1[2][7][4]

数组是对象
数组实例是从System.Array继承的对象。由于数组从BCL(Base Class Library)基类继承,它们也继承了很多有用的方法。
- Rank 返回数组维度数
- Length 返回数组长度(数组中所有元素的个数)
数组是引用类型,与所有引用类型一样,有数据的引用以及数据对象本身。引用在栈或堆上,而数组对象本身总在堆上。

尽管数组总是引用类型,但是数组元素可以是值类型也可以是引用类型。
- 如果存储的元素都是值类型,数组被称作值类型数组
- 如果存储的元素都是引用类型,数组被称作引用类型数组

一维数组和矩形数组
一维数组和矩形数组的语法非常相似,因此放在一起阐述。然后再单独介绍交错数组。
声明一维数组或矩形数组
声明一维数组或矩形数组,在类型和变量名称间使用一对方括号。
矩形数组声明示例:
-
可以使用任意多的秩说明符
-
不能在数组类型区域中放数组维度长度。秩是数组类型的一部分,而纬度长度不是类型的一部分
-
数组声明后,维度数就固定了。然而,纬度长度直到数组实例化时才确定
秩说明符 ↓ int[,,] firstArray; //三维整型数组 int[,] arr1; //二维整型数组 long[,] arr3; //三维long数组 long[3,2,6] SecondArray;//编译错误 ↑ 声明时不允许设置维度长度
和C/C++不同,方括号在基类型后,而不是在变量名称后。
实例化一维数组或矩形数组
要实例化数组,我们可以使用数组创建表达式。数组创建表达式由new运算符构成,后面是基类名称和一对方括号。方括号中以逗号分隔每个维度的长度。
例:
1int[] arr2=new int[4];//包含4个int的一维数组 2MyClass[] maArr=new MyClass[4];//包含4个MyClass引用的一维数组 3int[,,] arr3=new int[3,6,2];//三维数组,数组长度是3*6*2=36

与对象创建表达式不一样,数组创建表达式不包含圆括号-即使是对于引用类型数组。
访问数组元素
在数组中使用整型值作为索引来访问数组元素。
-
每个维度的索引从0开始
-
方括号内的索引在数组名称之后
int[] intArr1=new int[15]; intArr1[2]=10; int var1=intArr1[2]; int[,] intArr2=new int[5,10]; intArr2[2,3]=7; int var2=intArr2[2,3];
int[] myIntArray; myIntArray=new int[4]; for(int i=0;i<4;i++) { myIntArray[i]-i*10; } for(int i=0;i<4;i++) { Console.WriteLine("Value of element {0} = {1}",i,myIntArray[i]); }

初始化数组
数组被创建后,每个元素被自动初始化为类型的默认值。对于预定义类型,整型默认值是0,浮点型默认值是0.0,布尔型默认值是false,而引用类型默认值则是null。
例:一维数组的自动初始化
int[] intArr=new int[4];

显式初始化一维数组
-
初始值必须以逗号分隔,并封闭在一组大括号内
-
不必输入数组长度,因为编译器可以通过初始化值的个数来推断长度
-
注意,在数组创建表达式和初始化列表中间没有分隔符。即,没有等号或其他连接运算符
int[] intArr=new int[]{10,20,30,40};

显式初始化矩形数组
要显式初始化矩形数组,需遵守以下规则:
-
每个初始值向量必须封闭在大括号内
-
每个维度也必须嵌套并封闭在大括号内
-
除了初始值,每个维度的初始化列表和组成部分也必须使用逗号分隔
int[,] intArray2=new int[,]{{10,1},{2,10},{11,9}};

快捷语法
在一条语句中使用声明、数组创建和初始化时,可以省略语法的数组创建表达式部分。快捷语法如下:
1int[] arr1=new int[3]{10,20,30}; 2int[] arr1= {10,20,30}; 3int[,] arr=new int[2,3]{{0,1,2},{10,11,12}}; 4int[,] arr= {{0,1,2},{10,11,12}};
隐式类型数组
上面声明的数组都是显式指定数组类型。然而,和其他局部变量一样,数组可以是隐式类型的。
- 当初始化数组时,我们可以让编译器根据初始化语句的类型来推断数组类型。只要所有初始化语句能隐式转换为单个类型,就可以这么做
- 和隐式类型的局部变量一样,使用var关键字
虽然用var替代了显式数组类型,但是仍然需要在初始化中提供秩说明符。
1int[] intArr1=new int[]{10,20,30,40}; 2var intArr2=new []{10,20,30,40}; 3int[,] intArr3=new int[,]{{10,1},{2,10},{11,9}}; 4var intArr4=new [,]{{10,1},{2,10},{11,9}}; 5string[] sArr1=new string[]{"life","liberty","pursuit of happiness"}; 6var sArr2=new []{"life","liberty","pursuit of happiness"};
综合内容
例:综合示例
1var arr=new int[,]{{0,1,2},{10,11,12}}; 2for(int i=0;i<2;i++) 3{ 4 for(int j=0;j<3;j++) 5 { 6 Console.WriteLine("Element [{0},{1}] is {2}",i,j,arr[i,j]); 7 } 8}

交错数组
交错数组是数组的数组。与矩形数组不同,交错数组的子数组的元素个数可以不同。
例:二维交错数组
-
第一个维度长度是3
-
声明可以读作“jagArr是3个int数组的数组”
-
图中有4个数组对象-其中一个针对顶层数组,另外3个针对子数组
int[][] jagArr=new int[3][];

声明交错数组
交错数组的声明语法要求每个维度都有一对独立的方括号。数组变量声明中的方括号数决定了数组的秩。
和矩形数组一样,维度长度不能包括在数组类型的声明部分。
1int[][] SomeArr;//秩等于2 2int [][][] OhterArr;//秩等于3
快捷实例化
我们可以将数组创建表达式创建的顶层数组和交错数组的声明相结合。
int[][] jagArr=new int[3][];//3个子数组
不能在声明语句中初始化顶层数组之外的数组。
int[][] jagArr=new int[3][4];//不允许,编译错误
实例化交错数组
和其他类型数组不一样,交错数组的完全初始化不能在一个步骤中完成。由于交错数组是独立数组的数组-每个数组必须独立创建。
- 首先,实例化顶层数组
- 其次,分别实例化每个子数组,把新建数组的引用赋给它们所属数组的合适元素
例:实例化交错数组
1int[][] Arr=new int[3][]; 2Arr[0]=new int[]{10,20,30}; 3Arr[1]=new int[]{40,50,60,70}; 4Arr[3]=new int[]{80,90,100,110,120};

比较矩形数组和交错数组
矩形数组和交错数组的结构区别非常大。
例如,下图演示了3X3的矩形数组以及由3个长度为3的一维数组构成的交错数组的结构。
- 两个数组都保存9个整数,但是它们结构很不相同
- 矩形数组只有1个数组对象,而交错数组有4个数组对象

在CIL(Common Intermediate Laguage,公共中间语言)中,一维数组有特定的指令用于性能优化。矩形数组没有这些指令,并且不在相同级别进行优化。因此,有时使用一维数组(可以被优化)的交错数组比矩形数组(不能被优化)更有效率。
另一方面,矩形数组的编程复杂度要小得多,因为它会被作为一个单元而不是数组的数组。
foreach语句
foreach语句允许我们连续访问数组中的每个元素。
有关foreach语句的重点如下:
-
迭代变量是临时的,并且和数组中元素的类型相同。foreach语句使用迭代变量来相继表示数组中的每个元素
-
foreach语句的语法如下
- Type是数组中元素的类型。我们可以显式提供它的类型,或者使用var
- Identifier是迭代变量名
- ArrayName是数组名
- Statement是要为数组中每个元素执行一次的单条语句或语句块
foreach(Type Identifier in ArrayName)//显式 { Statement } foreach(var Identifier in ArrayName)//隐式 { Statement }
foreach如下方式工作:
- 从数组第一个元素开始并把它赋值给迭代变量
- 执行语句主体。在主体中,我们可以把迭代变量作为数组元素的只读别名
- 主体执行后,foreach语句选择数组中的下一个元素并重复处理
例:foreach示例
1int[] arr1={10,11,12,13}; 2foreach(int item in arr1) 3{ 4 Console.WriteLine("Item Value: {0}",item); 5}

迭代变量是只读的
迭代变量是只读的。但是,对于值类型数组和引用类型数组效果不一样。
对于值类型数组,这意味着在用迭代变量时,我们不能改变它们。
例:尝试修改迭代变量报错
1int[] arr1={10,11,12}; 2foreach(int item in arr1) 3{ 4 item++; 5}
对于引用变量,迭代变量保存的是数据的引用,而不是数据本身。所以,我们虽然不能改变引用,但是我们可以改变数据。
例:引用变量修改迭代变量
1class MyClass 2{ 3 public int MyField=0; 4} 5class Program 6{ 7 static void Main() 8 { 9 MyClass[] mcArray=new MyClass[4]; 10 for(int i=0;i<4;i++) 11 { 12 mcArray[i]=new MyClass(); 13 mcArray[i].MyField=i; 14 } 15 foreach(var item in mcArray) 16 { 17 item.MyField+=10; 18 } 19 foreach(var item in mcArray) 20 { 21 Console.WriteLine("{0}",item.MyField); 22 } 23 } 24}

foreach语句和多维数组
多维数组里,元素处理顺序是从最右边的索引号依次递增。
例:矩形数组示例
1class Program 2{ 3 static void Main() 4 { 5 int total=0; 6 int[,] arr1={{10,11},{12,13}}; 7 foreach(var element in arr1) 8 { 9 total+=element; 10 Console.WriteLine("Element:{0},Current Total:{1}",element,total); 11 } 12 } 13}

例:交错数组示例
1class Program 2{ 3 static void Main() 4 { 5 int total=0; 6 int[][] arr1=new int[2][]; 7 arr1[0]=new int[]{10,11}; 8 arr1[1]=new int[]{12,13,14}; 9 foreach(int[] array in arr1) 10 { 11 Console.WriteLine("Starting new array"); 12 foreach(int item in array) 13 { 14 total+=element; 15 Console.WriteLine("Item:{0},Current Total:{1}",item,total); 16 } 17 } 18 } 19}

数组协变
某些情况下,即使某对象不是数组的基类型,我们也可以把它赋给数组元素。这种属性叫做数组协变(array covariance)。在下面情况下使用数组协变。
- 数组是引用类型数组
- 在赋值的对象类型和数组基类之间有隐式转换或显式转换
由于派生类和基类之间总有隐式转换,因此总是可以把派生类对象赋值给基类声明的数组。
例:派生类、基类 数组协变
1class A{...} 2class B:A{...} 3class Program 4{ 5 static void Main() 6 { 7 A[] AArray1=new A[3]; 8 A[] AArray2=new A[3]; 9 AArray1[0]=new A();AArray1[1]=new A();AArray1[2]=new A(); 10 AArray2[0]=new B();AArray2[1]=new B();AArray2[2]=new B(); 11 } 12}

值类型数组没有协变
数组继承的有用成员
C#数组从System.Array类继承。它们从基类继承了很多有用的属性和方法。

1public static void PrintArray(int[] a) 2{ 3 foreach(var x in a) 4 { 5 Console.WriteLine("{0}",x); 6 } 7 Console.WriteLine(""); 8} 9static void Main() 10{ 11 int[] arr=new int[]{15,20,5,25,10}; 12 PrintArray(arr); 13 Array.Sort(arr); 14 PrintArray(arr); 15 Array.Reverse(arr); 16 PrintArray(arr); 17 Console.WriteLine(); 18 Console.WriteLine("Rank = {0},Length = {1}",arr.Rank,arr.Length); 19 Console.WriteLine("GetLength(0) = {0}",arr.GetLength(0)); 20 Console.WriteLine("GetType() = {0}",arr.GetType()); 21}

Clone方法
Clone方法为数组进行浅复制。即,它只创建了数组本身的克隆。如果是引用类型数组,它不会复制元素引用的对象。对于值类型数组和引用类型数组,效果不同。
- 克隆值类型数组会产生两个独立数组
- 克隆引用类型数组会产生指向相同对象的两个数组
Clone方法返回object类型的引用,它必须被强制转换成数组类型。
1int[] intArr1={1,2,3}; 2int[] intArr2=(int[])intArr1.Clone();
例:Clone值类型数组示例
1class Program 2{ 3 static void Main() 4 { 5 int[] intArr1={1,2,3}; 6 int[] intArr2=(int[])intArr1.Clone(); 7 intArr2[0]=100; 8 intArr2[1]=200; 9 intArr2[2]=300; 10 } 11}

例:Clone引用类型数组示例
1class A 2{ 3 public int Value=5; 4} 5class Program 6{ 7 static void Main() 8 { 9 A[] AArray1=new A[3]{new A(),new A(),new A()}; 10 A[] AArray2=(A[])AArray1.Clone(); 11 AArray2[0].Value=100; 12 AArray2[1].Value=200; 13 AArray2[2].Value=300; 14 } 15}

比较数组类型
下表总结了3中类型数组的重要相似点和不同点。
