数组
声明数组
数组是把多个同类型变量打包的类型。所以声明一个数组,相当于让c#替你同时声明多个变量。
var arr1 = new int[10];//构造一个int数组,它里面有10个int
如果你一开始就能决定好所有元素的数量和值,那么可以使用字面量进行声明。
字面量必须指定变量类型,不能使用var
进行判断。
int[] arr2 = [ 4, 5, 6, 7, 8, 9 ];
尽管这称作字面量,但他依然是通过构造产生的。因此不能作为常量值。
多维数组
一个方括号放在类型后面表示这种类型的数组。
并且,由于数组也是有效的类型,所以有数组的数组。
int[][] arr3 =new int [10][];//这是数组的数组。数量写在最后一个方括号里。
除此之外,还有多维数组,他在一个方括号内添加逗号表示维度的数量。
int[,] arr4 =new int [4 ,5];//这是二维的多维数组
上述例子中,多维数组会创建20个int
。
而数组的数组会创建10个int []
。并且这10个数组还需要你之后再自己进行构造和赋值。
所以这10个数组不一定是一样多的。
访问元素
数组的元素通过索引器访问。使用中括号表示。
int[] arr11 = new int[4];
arr11[0] = 12;
arr11[1] = 16;
arr11[2] = 64;
arr11[3] = arr11[0] + arr11[1];
Console.WriteLine(arr11[3] - arr11[2]);
索引器中要求的参数是Index
类型。
不过所有的int
类型都可以直接转为Index
类型。
Index index11 = 1;//表示正数第2个。索引从0开始计数,所以1是第二个。
Index index12 = ^1;//在前面加^表示总数量-1,也就是倒数第1个。
int i11 = 1;
Index index13 = i11;//Index可以直接接受int作为值
Index index14 = ^i11;//也可以在int变量前面加^
遍历
索引器接受的参数也可以使用变量。
这样就可以配合循环来遍历数组。
对于普通数组,使用Length
获取元素数量作为条件。
int[] arr12 = new int[10];
for (int i = 0; i < arr12.Length; i++)
{
arr12[i] = i * i;
}
for (int i = 0; i < arr12.Length; i++)
{
Console.WriteLine(arr12[i]);
}
对于多维数组,使用GetLength()
方法获取指定维度的元素数量作为条件。
int[,] arr13 = new int[4, 5];
for (int i = 0; i < arr13.GetLength(0); i++)//获取维度也是从0开始计数
{
for (int j = 0; j < arr13.GetLength(1); j++)
{
arr13[i, j] = i * j;
}
}
^1
不是运算符,而是语法糖。它要求编译器构造一个Index
类型的值。^1
不是字面量,而是指令。所以无法声明Index
的常量
数组长度
索引越界
对数组元素的访问,只能访问有效索引。
有效索引从0开始数,总数量等于数组长度。所以最大的有效索引为数组长度-1
而对于Index
类型来说,^1
表示倒数第一个元素。相当于arr[arr.Length - 1]
。
Index
的倒数最大有效值为数组的Length
。
无论使用int
还是Index
,如果访问了非法的索引,那么或报错索引越界
。
数组长度可以为0,此时任何索引都是非法的。
长度固定
之前的类型都是简单类型。他们没有内容。
任何对值的修改都是通过修改变量本身来完成的。
而数组的元素,是数组的内容
。
你可以在不改变数组变量的情况下改变他的内容。
但如果你要改变数组元素的数量,你就必须把这个数组变量给重新赋值。
也因为数组的元素是数组的
内容,所以你在使用他们前,不需要像变量一样先赋值初始值。
指针
引用类型
如果你直接用一个数组变量来给另一个数组变量赋值,你会发现他们会窜号。
int[] arr21 = new int[1];
int[] arr22 = arr21;
Console.WriteLine(arr21[0]);//0
Console.WriteLine(arr22[0]);//0
arr21[0] = 12;
Console.WriteLine(arr21[0]);//12
Console.WriteLine(arr22[0]);//12
这是因为数组是引用类型
。
他的值表明内容储存在哪,而自己不直接存储内容。
内容是指通过点出来,或通过索引器访问的东西。
解指针
表明内容储存在哪
这个东西可以称作一个指针。
类似于你桌面上的快捷方式,或者你看视频时分享给好友的视频链接。
就像你发网址而不是下载视频发视频。引用类型通常是内容非常大的东西。
比如数组能轻易声明几百万个int。完全复制会占用非常多的资源。
引用类型和值类型的区别在于,他的值是否连他的内容一起包含。
赋值的操作本质是一个复制。值类型因为储存内容,所以他的内容会一起复制。
引用类型因为自己不储存内容,所以如果想要访问他的内容
就要顺着内容储存在哪
这个信息去找到实际的内容。
那么如果引用类型直接复制,他们找到的内容就是同一份。
所以一方修改值,另一方就也会看到值被修改了。
托管类型
托管是指.Net可以控制的东西,比如分配,监视,回收,释放。
因此引用类型的指针值的更新(清理垃圾的时候会移动内容),
创建指针和解指针的操作,完全不需要你操心。
c#真正的指针需要通过修改配置来启用。
这些不正规的指针,都是托管指针。都是安全的。
引用变量
引用变量可以创建对变量的指针。对引用变量的任何操作都会解指针后对原变量进行操作。
int i31 = 6;
ref int ri31 = ref i31;//必须声明时赋值
Console.WriteLine(i31);//6
Console.WriteLine(ri31);//6,相当于访问i1
ri31 = 10;//直接赋值会作用到原变量上
Console.WriteLine(i31);//10,会被修改
Console.WriteLine(ri31);//10
int[] arr31 = new int[6];
ri31 = ref arr31[0];//可以重新赋值
Console.WriteLine(arr31[0]);//0
ri31 = 50;
Console.WriteLine(arr31[0]);//50
默认值
数组创建后,它里面的元素是默认值。
对于指定的类型可以使用字面量default
获得默认值。
对于没有指定的类型,可以在default
后加个括号指定类型。
bool b = default;
Console.WriteLine(default(char));
默认值的逻辑是,对这个类型所占据的内存都设置为0
。
- 数字类型会被解读
0
char
会被解读为一个字符编码为0
的字符。bool
会被解读为false
- 引用会被解读为
null
null
也是一个字面量,表示不引用任何数据。所有的引用类型都可以使用null值。
int[][] arr13 = new int[5][];//数组是一个有效的类型,所以数组也可以有数组
for (int i = 0; i < arr13.Length; i++)
{
arr13[i] = new int[6];//数组的默认值是null,无法使用
}
arr13[2][3] = 8;
null
值不能执行解指针操作。null
值变量本身可以使用,访问,比较。但不能访问内容。
string s1 = null;//字符串也是引用类型
Console.WriteLine(s1 == null);//仅访问变量
Console.WriteLine(s1 == "hello");//仅访问变量
Console.WriteLine("hello".Length);//获取内容,字符串的长度
Console.WriteLine("hello"[2]);//获取内容,第3个字符
Console.WriteLine(s1.Length);//尝试获取null的内容,报错
Console.WriteLine(s1[2]);//尝试获取null的内容,报错
字符串是不可改变的值。尽管可以通过索引访问他的字符,
但不能对他的字符进行赋值。任何对字符串的修改都要通过修改他的变量来完成。
字符串是引用类型,和数组有着相同的判断逻辑,即判断指针。
但是.Net会控制字符串的生成,并回收他们。每当你创建字符串时,
都会从记录的字符串里看看有没有一样的,如果有就给你现成的。所以字符串可以直接进行比较。
截取数组
复制
如果想复制数组的一小段内容,可以直接使用Range
作为索引。
和Index
类似,他可以使用特殊语法构造,但不是常量。
int[] arr41 = [ 4, 5, 6, 7, 8, 9 ];
var arr42 = arr41[0..^3];
for (int i = 0; i < arr42.Length; i++)
{
Console.WriteLine(arr42[i]);//4,5,6
}
Range
包含起始索引,不包含结束索引,即arr41[^3]
并不在arr42
中。
Range
可以省略开头或尾的索引,这样的话就表示从开头一直取,或者一直取到结尾。
也可以同时省略开头和结尾的索引,表示复制整个数组。
int[] arr43 = { 4, 5, 6, 7, 8, 9 };
var arr44 = arr43[..];
arr43[0] = 14;
Console.WriteLine(arr44[0]);//4
这个语法也可以作用于字符串。
另外,Range
的构造语法有个缺点,他的优先级太高了。
如果要对首尾的索引进行修正,需要打括号。
string s41 = "hello2.png";
string s42 = s41[..(s41.Length - 3)];
引用
引用类型的数据由.Net负责分配,监视,回收,释放。这个流程消耗的资源是非常高的。
在一些高性能代码中,只需要读取数组的一部分,或者是排序算法中只需要聚焦一部分。
可以使用Span
int[] arr45 = [ 4, 5, 6, 7, 8, 9 ];
var span41 = arr45.AsSpan();
Console.WriteLine(arr45[0]);//4
span41[0] = 99;
Console.WriteLine(arr45[0]);//99
Span
类型就像引用变量的数组。引用变量是特殊类型,你没有办法直接声明他的数组。
对Span
的值进行修改,会作用到原本的数组上。Span
也可以使用Range
进行截取。
这样会构造一个新的Span
,但Span
是值类型,构造Span
的消耗比构造一个新的数组小得多。