首页 / C# 入门教程 / 数组常用操作

C# 入门教程

数组常用操作

本教程共 100 篇 · 第 21 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程编程语言数组Array

21. 数组常用操作

本节目标:学会用 Array 类给数组排序、查找、复制、反转,并能手写查找最大值与最小值的算法。

上一章我们认识了数组(array)的声明、索引与遍历。但数组建好之后,光靠索引读写还不够用——排序、查找、复制这些动作几乎每个程序都要用到。这章就把这些”高频动作”一次讲透。

为什么需要 Array 类

你可能好奇:int[] numbers 明明是个数组变量,前面没加任何前缀,为什么能直接调用 Array.Sort(numbers)?原因在于,你写的 int[] 本质上就是 System 命名空间里 Array 类的一个具体实例。

C# 在 System 命名空间里提供了一个 Array 类(注意首字母大写),它把排序、查找、复制、反转等一堆常用功能打包成了静态方法(static method)。我们不必每次都手写循环,直接调用即可,既省事又不容易写错。

数组虽然简单,但排序、查找这类动作如果每次都从零写循环,要写好几行,还容易在边界条件上栽跟头。Array 类相当于官方替我们打磨好的工具箱。

Note

Array 是一个抽象类,不能直接 new Array()。它的方法都通过 Array.Sort(...) 这种形式调用,作用在”具体的数组实例”上。你可以把它理解成一把万能扳手,对螺丝(数组)直接拧。

我们先用 C# 12 的集合表达式建一个数组,后面都用它做实验。

int[] numbers = [5, 2, 9, 1, 7, 3];
foreach (var n in numbers)
{
    Console.Write($"{n} ");
}
// 输出:5 2 9 1 7 3

获取数组长度

Length 属性告诉我们数组里有多少个元素。它是只读的,因为数组一旦创建,大小就固定了。多维数组还能用 GetLength(维度) 拿到某一维的长度。

int[] numbers = [5, 2, 9, 1, 7, 3];
Console.WriteLine($"数组长度:{numbers.Length}");
// 输出:数组长度:6
Tip

数组是固定大小的。想随时增删元素,请看下一个章节的集合 List<T>。在数组上,Length 永远不变,这是它和 List 最大的区别。把”固定长度”当成数组的天性,反而能帮你在需要动态的场景果断换工具。

排序:Array.Sort

Sort 方法会原地排序,把数组排成升序。它不返回新数组,而是直接改动原来的数组。

int[] numbers = [5, 2, 9, 1, 7, 3];
Array.Sort(numbers);
foreach (var n in numbers)
{
    Console.Write($"{n} ");
}
// 输出:1 2 3 5 7 9

Sort 默认升序。要降序可以先 SortReverse,或传入自定义比较器(后续 LINQ 章节会细讲)。对字符串排序按当前文化的排序规则进行,比如中文会按拼音或笔画顺序,具体取决于系统区域设置。

string[] fruits = ["香蕉", "苹果", "橙子"];
Array.Sort(fruits);
foreach (var f in fruits)
{
    Console.Write($"{f} ");
}
Warning

Array.Sort 用的是快速排序的变体(introsort),平均很快,但它是不稳定排序——相等元素的相对顺序可能被打乱。如果你对”相等元素谁在前”有要求(比如先按分数排、再保持原录入顺序),简单用 Sort 会丢顺序,那时需要借助 LINQ 的 OrderBy 稳定排序。初学先记住”默认升序、原地改”即可。

反转:Array.Reverse

Reverse 把元素顺序掉个头,同样是原地操作,不返回新数组。

int[] numbers = [1, 2, 3, 4];
Array.Reverse(numbers);
foreach (var n in numbers)
{
    Console.Write($"{n} ");
}
// 输出:4 3 2 1

查找:Array.Find 与条件

Find 接收一个”条件”(官方叫谓词 predicate),返回第一个满足条件的元素。这个条件我们用 Lambda 写成 x => x > 5 这样的形式。

int[] numbers = [5, 2, 9, 1, 7, 3];
int firstBig = Array.Find(numbers, x => x > 5);
Console.WriteLine($"第一个大于 5 的数:{firstBig}");
// 输出:9

Find 只返回”第一个”匹配项。要收集所有匹配,用 Array.FindAll,它返回一个装满了结果的新数组。

int[] numbers = [5, 2, 9, 1, 7, 3];
int[] bigOnes = Array.FindAll(numbers, x => x > 4);
foreach (var n in bigOnes) Console.Write($"{n} "); // 5 9 7
Tip

FindFindAll 背后要逐个扫描元素,元素越多越慢(O(n))。如果你的数组已经排好序,查找请用下面的 BinarySearch,能快很多。

定位索引:IndexOf 与 LastIndexOf

除了按条件找元素,还能直接找某个值第一次、最后一次出现的位置。

int[] numbers = [1, 5, 3, 5, 9];
Console.WriteLine(Array.IndexOf(numbers, 5));     // 1
Console.WriteLine(Array.LastIndexOf(numbers, 5)); // 3

找不到时,IndexOf 返回 -1,而不是抛异常,这点比 Find 更适合先做判断。所以”判断数组里有没有某个值”,用 IndexOf(...) >= 0 比用 Find 更直观。

二分查找:BinarySearch

数组已经排好序时,BinarySearch 用二分法查找,比逐个比较快得多(元素越多优势越大,复杂度约 O(log n))。

int[] numbers = [1, 2, 3, 5, 7, 9];
int pos = Array.BinarySearch(numbers, 5);
Console.WriteLine($"5 在索引 {pos}");
Warning

BinarySearch 要求数组先排好序。没排序就查,结果不可靠,甚至可能给出错误的索引。找不到时返回的是负数(具体是”插入点的按位取反值”),别当成”找到了索引 0”。实战里先用 Array.SortBinarySearch 是标准组合。

批量判断:Exists 与 TrueForAll

Exists 问”有没有满足条件的”;TrueForAll 问”是不是全部都满足”。

int[] numbers = [5, 2, 9, 1, 7, 3];
bool anySmall = Array.Exists(numbers, x => x < 2);
bool allPositive = Array.TrueForAll(numbers, x => x > 0);
Console.WriteLine($"有小于2的:{anySmall},全是正数:{allPositive}");

这两个方法其实是用 Lambda 把”有没有 / 是不是都”浓缩成一句话,比手写循环清爽。后续 LINQ 的 AnyAll 与它一脉相承。

复制:Array.Copy 与 Clone

复制数组有两种常见做法。Copy 更灵活,能指定从哪复制到哪、复制几个。

int[] source = [1, 2, 3, 4, 5];
int[] part = new int[3];
Array.Copy(source, part, 3);   // 从 source 复制前 3 个到 part
foreach (var n in part)
{
    Console.Write($"{n} ");
}
// 输出:1 2 3

Clone 则是整份浅拷贝,返回的是 object,需要强转回数组类型。

int[] source = [1, 2, 3];
int[] copy = (int[])source.Clone();
copy[0] = 99;                   // 改动副本,不影响原数组
Console.WriteLine(source[0]);  // 输出:1
Tip

值类型数组(如 int[])Clone 之后两份互不影响。引用类型数组 Clone 只复制”引用”,里面的元素对象仍是同一批——改了对象内容,两边都会变。这种”浅拷贝”陷阱在数组装自定义类时要特别留意。

手写算法:查找最大值与最小值

不依赖现成方法,自己写循环也能找最值。核心思路是:先假定第一个元素最大,再挨个比较。

int[] numbers = [5, 2, 9, 1, 7, 3];
int max = numbers[0];
foreach (var n in numbers)
{
    if (n > max)
    {
        max = n;
    }
}
Console.WriteLine($"最大值:{max}");
// 输出:最大值:9

找最小值只要把比较符号反过来。想找最大值所在的索引,就保存索引而不是值。这个练习的价值不在”怎么找最值”——Array 其实没有现成 Max 方法(Max 在 LINQ 里),而是帮你把”循环 + 比较 + 更新初值”这套思维练熟,后面写各种统计都靠它。

Note

新手常犯的坑:循环从索引 0 开始,却把 max 初始化成 0。若数组全是负数(如 [-5, -2, -9]),结果就错了,因为 0 比所有负数都大。正确做法是拿第一个元素当初值,或者确认数组里有正数时再用 0。

求和与平均:动手算一遍

最值之外,求和和平均也是数组常见操作。虽然 LINQ 有 Sum/Average,但自己写一遍更能体会循环累加:

int[] numbers = [5, 2, 9, 1, 7, 3];
int sum = 0;
foreach (var n in numbers) sum += n;
double avg = (double)sum / numbers.Length;
Console.WriteLine($"总和 {sum},平均 {avg:F2}");
// 输出:总和 27,平均 4.50

注意 (double)sum 这一步是为了把整数除法变成浮点除法,否则 27/6 会得到 4 而非 4.5

清空与调整大小

Clear 把一段元素重置为默认值(数字变 0)。Resize 则能改变一维数组的长度,旧数据会尽量保留。

int[] numbers = [1, 2, 3, 4];
Array.Clear(numbers, 0, 2);    // 前两个清零
foreach (var n in numbers)
{
    Console.Write($"{n} ");
}
// 输出:0 0 3 4

Array.Resize(ref numbers, 6);  // 扩到 6 个,新位置是 0
Console.WriteLine($"新长度:{numbers.Length}");
// 输出:新长度:6
Tip

Resize 其实是创建新数组再复制,原变量会被指向新数组,所以必须传 ref。频繁调整大小,不如直接用 List<T>——List 内部也是数组,但扩容逻辑帮你自动做好了。

综合演练

把几个方法串起来:先排序,再找最大值,最后复制前三个到新数组。

int[] numbers = [5, 2, 9, 1, 7, 3];
Array.Sort(numbers);
int max = numbers[numbers.Length - 1];
int[] top3 = new int[3];
Array.Copy(numbers, top3, 3);
Console.WriteLine($"最大值是 {max},前三小是:");
foreach (var n in top3) Console.Write($"{n} "); // 1 2 3

数组方法的使用心法

讲完一堆方法,给你几条能直接上手的原则:

  1. 分清”原地改”还是”返回新数组”Sort/Reverse/Clear/Resize 是原地改;Find/FindAll/Clone 才会产出新的东西。改之前想清楚要不要先备份。
  2. 查值优先 IndexOf:只是判断”在不在”,IndexOf(...) >= 0Find 更直白,也不会因找不到而返回该类型的默认值造成误判。
  3. 排序后再查用 BinarySearch:数据量大又已排序,别用 Find 挨个扫。
  4. 数组定长,别硬撑:需要频繁增删,转 List<T>
Warning

foreach 遍历数组时,不要尝试”边遍历边改长度”(比如删除元素)。数组长度固定,虽然不能删,但如果用索引循环并改了内容,也要当心逻辑错位。遍历期间保持数组结构不变,是最稳的做法。

小结

数组自身容量固定,但 Array 类替我们准备好了排序、反转、查找、复制等工具方法。记住它们大多是”原地修改”,调用前想清楚是否要备份。找最值的算法虽简单,却是理解循环与比较的好练习。下一章的集合 List<T> 会在这个基础上解决”动态增删”的痛点。