数组保存固定数量、类型相同的元素,这些元素在内存中依次相邻存放。长度在创建时确定,你通过从零开始的索引读写元素,之后长度不会再改变。
声明和初始化数组
数组的类型写作元素类型后面加上 []。创建数组有几种写法:
输出:
90, 0, 0, 72
Ben
2 prices, 3 days
new int[4] 会把每个元素都设为该类型的默认值:数字是 0,bool 是 false,字符串和其他引用类型是 null。简写形式 { "Ana", "Ben" } 只能用在声明中;给已有变量赋值时,需要写成 names = new[] { "Dev", "Eli" };。
长度、索引和最后一个元素
索引的范围是 0 到 Length - 1。Length 是属性,所以不加括号(这一点和 LINQ 的 Count() 方法不同)。
输出:
Count: 5
First: 18
Last: 16
Caught IndexOutOfRangeException
在边界之外读写会在运行时抛出 IndexOutOfRangeException,编译器不会帮你发现。最常见的原因是把循环条件写成了 i <= arr.Length,而不是 i < arr.Length。
C# 8 及以后的版本新增了“从末尾索引”和范围语法,取最后一个元素或截取切片时更简洁:
int last = temps[^1]; // 16
int[] middle = temps[1..4]; // { 21, 25, 19 }, a new array
遍历数组
for 循环能拿到索引;foreach 直接给你每个元素,只读取时用它更清晰。
输出:
4.95 | 13.20 | 3.58
Total: 21.73
foreach 的循环变量是只读的:在循环里写 p = 0; 会编译出错。要修改元素,请用带索引的循环。
排序、反转和查找
这些辅助方法都在静态类 Array 中。Array.Sort 原地排序且没有返回值,所以原来的顺序就没了。
输出:
Ascending: 60, 72, 79, 88, 95
Descending: 95, 88, 79, 72, 60
Index of 60: 4
Index of 99: -1
Any above 90: True
First below 75: 72
Ana, ben, chloe
fig, kiwi, banana
值不存在时,Array.IndexOf 返回 -1。没有匹配项时,Array.Find 返回该类型的默认值(这里是 0),对数字来说这会产生歧义;在意这一点时请用 Array.FindIndex。一次调用完成降序排序的写法是 Array.Sort(scores, (a, b) => b.CompareTo(a))。
如果想得到排序结果又不动原数组,LINQ 会返回一个新数组:var sorted = scores.OrderBy(s => s).ToArray();。参见 LINQ。
调整大小:数组不能增长
数组长度是固定的。Array.Resize 会按指定大小创建一个新数组,复制元素,然后更新你通过 ref 传入的变量。
输出:
4
2
True
False
每次调整大小都会复制所有元素。在循环里一次加一个元素地扩大数组很慢,而这正是 List<T> 替你做的事,它通过把内部数组的容量翻倍,做得更高效。
数组是引用类型:复制
把一个数组赋值给另一个变量,复制的是引用,而不是元素。两个变量名随后指向同一个数组。
输出:
100
2
0, 100, 2, 3, 0
把数组传给方法时也是如此:方法可以修改调用方的元素。Clone、Array.Copy、CopyTo 和 LINQ 的 ToArray() 都是浅复制。对于对象数组,新数组保存的是指向同一批对象的引用,所以修改 copy[0].Name 仍然会改变 original[0] 看到的内容。
用 == 比较数组比较的是引用。要比较内容,请使用 System.Linq 中的 a.SequenceEqual(b)。
二维数组(多维数组)
矩形网格的声明方式是在方括号里加一个逗号,用 [row, col] 访问元素。
输出:
Rows: 2
Cols: 3
Length: 6, Rank: 2
. X .
. . X
二维数组的 Length 是元素总数(2 行 3 列的网格是 6),所以嵌套循环要用 GetLength(0) 和 GetLength(1)。Rank 是维数。三维数组的用法相同:new int[4, 4, 4] 和 cube[x, y, z]。用 foreach 遍历二维数组时,会逐行访问每个元素。
交错数组(数组的数组)
交错数组 int[][] 的元素本身也是数组,每个内层数组都可以有自己的长度。它适合“每位顾客的订单”这类每行大小不同的数据。
输出:
Customer 0: 2 orders, 20, 35
Customer 1: 1 orders, 12
Customer 2: 4 orders, 8, 15, 40, 22
True
new int[3][] 只创建外层数组;每一行一开始都是 null,在给某行赋值之前访问 orders[0][0] 会抛出 NullReferenceException。new int[3][4] 这种写法是不允许的,每一行都要你自己创建。
真正的网格(棋盘、矩阵、像素)用 [,];各行长度不同,或者需要整行交换时,用 [][]。
数组与 List 对比
数组 T[] | List<T> | |
|---|---|---|
| 大小 | 创建时固定 | 可增可减 |
| 元素个数 | Length 属性 | Count 属性 |
| 添加或删除 | 不支持(只有 Array.Resize) | Add、Insert、Remove |
| 多维 | [,] 和 [][] | 只能是列表的列表 |
| 典型用途 | 大小已知、查找表、缓冲区 | 会变化的集合 |
两者互相转换都只需一次调用:new List<int>(arr) 或 arr.ToList(),以及 list.ToArray()。List<T> 就是可以增长的数组。
常见错误
- 在循环中写
i <= arr.Length。 最后一个有效索引是Length - 1,这样写会抛出IndexOutOfRangeException。 - 直接打印数组。
Console.WriteLine(arr)打印的是System.Int32[];请用string.Join。 - 以为
Array.Sort会返回排好序的数组。 它的返回类型是void,是原地排序。 - 用
=复制数组。 这只复制了引用;请用Clone()或ToArray()。 - 交错数组的行还没创建就使用。
new T[n][]的每一行在赋值之前都是null。 - 用
Length获取二维数组的行数。 它统计的是全部元素;请用GetLength(0)。
常见问题
C# 中如何获取数组的长度?
使用 Length 属性:scores.Length。它是属性而不是方法,所以不加括号。对二维数组来说,Length 是元素总数;用 GetLength(0) 获取行数,用 GetLength(1) 获取列数。
C# 中如何对数组排序?
Array.Sort(arr) 会原地把数组按升序排列。想要降序,可以在排序后调用 Array.Reverse(arr),或者传入一个比较函数:Array.Sort(arr, (a, b) => b.CompareTo(a))。如果想得到排好序的副本并保留原数组,用 LINQ:arr.OrderBy(x => x).ToArray()。
C# 中二维数组和交错数组有什么区别?
二维数组 int[,] 是一整块矩形区域:每一行的列数都相同,用 grid[row, col] 访问。交错数组 int[][] 是“数组的数组”:每一行都是一个独立的数组,长度可以各不相同,用 rows[i][j] 访问。交错数组的每一行都必须先创建再使用,否则它是 null。
C# 中能改变数组的大小吗?
不能。数组的长度在创建时就固定了。Array.Resize(ref arr, newSize) 看起来像是改变了大小,实际上它分配了一个新数组,把元素复制过去,再让你的变量指向新数组。如果大小经常变化,请使用 List<T>。
为什么打印数组会显示 System.Int32[]?
Console.WriteLine(arr) 会调用 arr.ToString(),而数组没有重写这个方法,所以你得到的是类型名。应该先把元素拼接成一个字符串:Console.WriteLine(string.Join(", ", arr))。
C# 中应该用数组还是 List?
元素个数已知且固定时用数组,比如 12 个月份或一个棋盘。元素会随时间增加或删除时用 List<T>。两者按索引访问都是常数时间,而且 List<T> 内部其实就是用数组来存储元素的。