目录
一、数组
1、定义
- 元素 数组的独立数据项称作元素。数组的所有元素必须是相同类型的,或继承自相同的类型;
- 秩/维度 数组的维度可以为任何正数,数组的维度数称作秩;
- 维度长度 数组的每一个维度有长度,就是这个方向的位置个数;
- 数组长度 数组的所有维度中的元素总数称为数组的长度;
2、重要细节
3、数组的类型
C#提供了两种类型的数组:
- 一维数组可以为认为是单行元素或元素向量;
- 多维数组是由主向量中的位置组成的,每一个位置本身又是一个数组,称为子数组,子数组向量中的位置本身又是一个子数组;
另外,有两种类型的多维数组:矩形数组和交错数组
- 矩形数组
- 某个维度的所有子数组具有相同长度的多维数组;
- 不管有多少维度,总是使用一组方括号;
int x = myArray2[4, 6, 1] //一组方括号
- 交错数组
- 每个子数组都是独立数组的多维数组;
- 可以有不同长度的子数组;
- 为数组的每一个维度使用一对方括号;
jagArray1[2][7][4] //3组方括号
三、数组是对象
上图展示了数组的结构。
上图说明数组的元素既可以是值,也可以是引用。
四、一维数组和矩形数组
一维数组和矩形数组的语法非常相似。
声明一维数组或矩形数组
要声明一维数组或矩形数组,可以在类型和变量名之间使用一对方括号。
方括号内的逗号就是秩说明符(秩就是维度数),他们制定了数组的维度数,秩就是逗号数量加1,比如,没有逗号代表一维数组,一个逗号代表二维数组,以此类推。
基类和秩说明符构成了数组类型。
秩说明符=1
⬇
long[ ] secondArray;
⬆
数组类型
上述代码声明了long的一维数组,数组类型是long[],读作“long数组”。
秩说明符
↓
int[,,] firstArray; //三维整型数组
int[,] arr1; //二维整型数组
long[,] arr3; //三维long数组
long[3,2,6] SecondArray;//编译错误
↑
声明时不允许设置维度长度
上述代码展示了矩形数组声明的示例,注意以下几点:
- 可以使用任意多个秩说明符;
- 不能在数组类型区域中放数组维度长度,秩是数组类型的一部分,而维度长度不是类型的一部分;
- 数组声明后,维度就是固定的了,然而,维度长度直到数组实例化时才会确定;
五、实例化一维数组或矩形数组
要实例化数组,可以使用数组创建表达式,数组创建表达式由new运算符构成,后面是基类名称和一对方括号,方括号中以逗号分隔每一个维度的长度。
int[] arr2=new int[4];//包含4个int的一维数组
MyClass[] maArr=new MyClass[4];//包含4个MyClass引用的一维数组
int[,,] arr3=new int[3,6,2];//三维数组,数组长度是3*6*2=36
说明
与对象表达式不一样,数组创建表达式不包括圆括号——即使是对于引用类型数组。
六、访问数组元素
七、初始化数组
1、显式初始化一维数组
int[] intArr=new int[]{10,20,30,40};
2、显式初始化矩形数组
int[,] intArray2=new int[,]{{10,1},{2,10},{11,9}};
3、初始化矩形数组的语法点
4、快捷语法
int[] arr1=new int[3]{10,20,30};
int[] arr1= {10,20,30};
int[,] arr=new int[2,3]{{0,1,2},{10,11,12}};
int[,] arr= {{0,1,2},{10,11,12}};
5、隐式类型数组
int[] intArr1=new int[]{10,20,30,40};
var intArr2=new []{10,20,30,40};
int[,] intArr3=new int[,]{{10,1},{2,10},{11,9}};
var intArr4=new [,]{{10,1},{2,10},{11,9}};
string[] sArr1=new string[]{"life","liberty","pursuit of happiness"};
var sArr2=new []{"life","liberty","pursuit of happiness"};
注意
在隐式类型intArr4的声明中,我们仍然需要在初始化中提供秩说明符。
6、综合内容
八、交错数组
交错数组是数组的数组,与矩形数组不同,交错数组的子数组的元素个数可以不同。
int[][] jagArr = new int[3][]; //声明并创建顶层数组
... //声明并创建子数组
上述代码声明了一个二维交错数组:
- 第一个维度的长度是3;
- 声明可以读作“jagArr是3个int数组的数组”;
- 注意,图中有4个数组对象,其中一个针对顶层数组,另外3个针对子数组;
1、声明交错数组
交错数组的声明语法要求每一个维度都有一对独立的方括号,数组变量声明中的方括号数量决定了数组的秩。
- 交错数组的维度可以是大于1的任意整数;
- 和矩形数组一样,维度长度不能包含在数组类型声明部分;
int[][] SomeArr;//秩等于2
int [][][] OhterArr;//秩等于3
2、快捷实例化
我们可以将用数组创建表达式创建的顶层数组和交错数组的声明相结合。
int[][] jagArr=new int[3][];//3个子数组
不能在声明语句中初始化顶层数组之外的数组。
不允许
⬇
int[][] jagArr=new int[3][4];//不允许,编译错误
⬆
允许
3、实例化交错数组
和其他类型的数组不一样,交错数组的初始化不能在一个步骤中完成。由于交错数组是独立数组的数组,所以每个数组必须独立创建,实例化完整的交错数组需要如下步骤:
- 实例化顶层数组;
- 分别实例化每一个子数组,把新建数组的引用付给它们所属数组的合适元素;
int[][] Arr = new int[3][]; //1.实例化顶层数组
Arr[0] = new int[]{10, 20, 30}; //2.实例化子数组
Arr[1] = new int[]{40, 50, 60, 70}; //3.实例化子数组
Arr[2] = new int[]{80, 90, 100, 110, 120}; //4.实例化子数组
4、交错数组中的子数组
由于交错数组中的子数组本身就是数组,因此交错数组中也可能有矩形数组。
static void Main(string[] args)
{
int[][,] Arr; //带有二维数组的交错数组
Arr = new int[3][,]; //实例化带有3个二维数组的交错数组
Arr[0] = new int[,] { { 10, 20 }, { 100, 200 } };
Arr[1] = new int[,] { { 30, 40, 50 }, { 300, 400, 500 } };
Arr[2] = new int[,] { { 60, 70, 80, 90 }, { 600, 700, 800, 900 } };
for(int i=0; i<Arr.GetLength(0); i++)
{
for(int j = 0; j < Arr[i].GetLength(0); j++)
{
for(int k = 0; k < Arr[i].GetLength(1); k++)
{
Console.WriteLine($"[{i}][{j},{k}] = {Arr[i][j, k]}");
}
Console.WriteLine();
}
Console.WriteLine();
}
}
执行结果:
九、比较矩形数组和交错数组
矩形数组和交错数组的结构区别非常大。
如上图,演示了3×3的矩形数组以及一个由3个长度为3的一维数组构成的交错数组的结构:
- 两个数组都保存了9个整数,但是它们的结构有很大不同;
- 矩形数组只有单个数组对象,而交错数组由4个数组对象;
在CIL(公共中间语言指令组)中,一维数组有特定的性能优化指令,矩形数组没有这些指令,并且不在相同级别进行优化。因此,有时使用一维数组(可以被优化)的交错数组比矩形数组(不能被优化)更高效。
另一方面,矩形数组的编程复杂度要低得多,因为它会被作为一个单元而不是数组的数组。
十、foreach语句
1、迭代变量是只读的
由于迭代变量的值是只读的,所以它不能改变。但是,这对于值类型数组和引用类型数组而言效果不一样。
对于值类型数组,这意味着在用迭代变量表示数组元素的时候,我们不能改变它们。
int[] arr1 = {10,11,12,13};
foreach(int item in arr1)
item++; //编译错误,不得改变变量值
对于引用类型数组,我们仍然不能改变迭代变量,但是迭代变量只是保存了数据的引用,而不是数据本身,因此,虽不能改变引用,但我们可以通过迭代变量改变数据。
class MyClass
{
public int MyField = 0;
}
class Program
{
static void Main()
{
MyClass[] mcArray = new MyClass[4]; //创建数组
for(int i = 0; i < mcArray.Length; i++)
{
mcArray[i] = new MyClass(); //创建类对象
mcArray[i].MyField = i; //设置字段
}
foreach (MyClass item in mcArray)
item.MyField += 10; //改变数据
foreach (MyClass item in mcArray)
Console.WriteLine($"{item.MyField}");
}
}
执行结果:
2、foreach语句和多维数组
在多维数组中,元素的处理次序是最右边的索引号最先递增,当索引从0到长度减1时,开始递增它左边的索引,右边的索引被重置成0.
(1)矩形数组示例
class Program
{
static void Main()
{
int[,] arr1 = { { 10, 11 }, { 12, 13 } };
int total = 0;
foreach(var element in arr1)
{
total += element;
Console.WriteLine($"Element: {element}, Current Total: {total}");
}
}
}
执行结果:
(2)交错数组示例
交错数组是数组的数组,所以我们必须为交错数组中的每一个维度使用独立的foreach语句,foreach语句必须嵌套以确保每一个嵌套数组都被正确处理。
class Program
{
static void Main()
{
int[][] arr1 = new int[2][];
arr1[0] = new int[] { 10, 11 };
arr1[1] = new int[] { 12, 13, 14 };
int total = 0;
foreach(int[] arr in arr1) //处理顶层数组
{
Console.WriteLine("Starting new array");
foreach(int item in arr) //处理第二层数组
{
total += item;
Console.WriteLine($"Item: {item}, Current Total: {total}");
}
}
}
}
执行结果:
十一、数组协变
在某些情况下,即使某个对象不是数组的基类型,也可以把它赋值给数组元素,这种属性叫作数组协变。
在下面的情况下可以使用数组协变:
- 数组是引用类型数组;
- 在赋值的对象类型和数组基类型之间有隐式转换或显式转换;
由于在派生类和基类之间总是有隐式转换,因此总是可以将一个派生类的对象赋值给为基类声明的数组。
class A{...} //基类
class B:A{...} //派生类
class Program
{
static void Main()
{
//两个A[]类型的数组
A[] AArray1=new A[3];
A[] AArray2=new A[3];
//普通:将A类型的对象赋值给A类型的数组
AArray1[0]=new A();AArray1[1]=new A();AArray1[2]=new A();
//协变:将B类型的对象赋值给A类型的数组
AArray2[0]=new B();AArray2[1]=new B();AArray2[2]=new B();
}
}
说明
值类型没有协变。
十二、数组继承的有用成员
C#数组派生自System.Array类,它们可从基类继承很多有用的属性和方法。
Clone方法
Clone方法为数组进行浅复制,也就是说,它只创建了数组本身的克隆。如果是引用类型数组,它不会复制元素引用的对象。对于值类型数组和引用类型数组而言,这有不同的结果。
- 克隆值类型数组会产生两个独立数组;
- 克隆引用类型数组会产生指向相同对象的两个数组;
Clone方法返回object类型的引用,它必须被强制转换成数组类型。
int[] intArr1={1,2,3};
int[] intArr2=(int[])intArr1.Clone();
克隆值类型数组示例
class Program
{
static void Main()
{
int[] intArr1={1,2,3};
int[] intArr2=(int[])intArr1.Clone();
intArr2[0]=100;
intArr2[1]=200;
intArr2[2]=300;
}
}
克隆引用类型数组示例
class A
{
public int Value=5;
}
class Program
{
static void Main()
{
A[] AArray1=new A[3]{new A(),new A(),new A()};
A[] AArray2=(A[])AArray1.Clone();
AArray2[0].Value=100;
AArray2[1].Value=200;
AArray2[2].Value=300;
}
}
十三、比较数组类型
十四、数组与ref返回和ref局部变量
利用ref返回功能,可以把一个引用作为返回值传到方法体外,而利用ref局部变量,你可以在调用域内使用这个引用。
class Program
{
public static ref int PointerToHighestPositive(int[] numbers)
{
int heighest = 0;
int indexOfHighest = 0;
for(int i = 0; i < numbers.Length; i++)
{
if (numbers[i] > heighest)
{
indexOfHighest = i;
heighest = numbers[indexOfHighest];
}
}
return ref numbers[indexOfHighest];
}
static void Main()
{
int[] scores = { 5, 80 };
Console.WriteLine($"Before: {scores[0]}, {scores[1]}");
ref int locationOfHigher = ref PointerToHighestPositive(scores);
locationOfHigher = 0; //change the value through ref local
Console.WriteLine($"After: {scores[0]}, {scores[1]}");
}
}
执行结果: