C# 一维数组的遍历方式与常用内置方法:玩转你的「储物柜」
引言:从翻箱倒柜到批量处理
上一篇我们学会了怎么建「储物柜」——创建数组、放数据、取数据。但现实开发中,你很少只操作单个元素,更多时候需要:
- 批量查看:把全班 50 个学生的成绩都打印出来
- 批量修改:给所有商品打 8 折
- 批量统计:计算平均分、找出最高分和最低分
- 批量处理:排序、筛选、复制
这就像你有一排储物柜,现在要挨个检查、整理、统计。怎么高效地做这些事?C# 提供了三种遍历方式和一大波内置方法,本文带你一一掌握。
一、基础铺垫:回顾一维数组的核心概念
在开始遍历之前,先确保你已经掌握了数组的基础。如果你还不熟悉,建议先阅读上一篇《C# 一维数组的定义和基础使用》。
1.1 什么是数组(30 秒回顾)
数组 = 一排固定数量、相同类型的储物柜,通过从 0 开始的索引访问。
// 创建一个有 5 个柜子的整数数组
int[] scores = new int[5]; // 动态初始化:只定长度,默认都是 0
scores[0] = 85; // 通过索引赋值
// 创建时直接放好东西
int[] numbers = { 10, 20, 30 }; // 静态初始化:长度自动为 3
// 让编译器猜类型
var prices = new[] { 9.9, 19.5 }; // 隐式初始化:推断为 double[]关键点:
Length属性获取长度(不加括号)- 合法索引范围:
0到Length - 1 - 越界会抛出
IndexOutOfRangeException
1.2 为什么要遍历
想象你是班主任,手里拿着 50 张成绩单。你会一张一张翻看,而不是只看第一张。遍历就是「挨个访问」数组里的每个元素,是批量处理数据的第一步。
二、核心章节 1:三种遍历方式全解析
遍历就是把数组里的每个元素挨个访问一遍。C# 提供三种主流方案,各有适用场景。
2.1 for 循环遍历:带索引的全能选手
是什么:通过索引变量 i 从 0 走到 Length - 1,逐个访问元素。
怎么用:
using System;
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78, 60, 95 };
// 标准 for 遍历:i 从 0 开始,小于 Length,每轮加 1
for (int i = 0; i < scores.Length; i++)
{
Console.WriteLine($"第 {i + 1} 位同学:{scores[i]} 分");
}
// for 循环还能修改元素:给每人加 5 分
for (int i = 0; i < scores.Length; i++)
{
scores[i] += 5; // 通过索引直接修改原数组
}
Console.WriteLine($"加分后第一位:{scores[0]}"); // 输出:90
}
}逐行拆解:
int i = 0—— 索引从 0 开始(第一个元素)i < scores.Length—— 注意是小于不是小于等于!当i等于Length时已经越界i++—— 每轮结束后索引加 1,走向下一个元素scores[i] += 5—— 通过索引直接修改原数组的元素
什么时候用:
- ✅ 需要索引(如「第 3 个元素」)
- ✅ 需要修改元素
- ✅ 需要逆序遍历(
for (int i = arr.Length - 1; i >= 0; i--)) - ✅ 需要跳过某些元素(如每隔一个取一个)
优缺点:
- 优点:功能最全,能拿到索引,能改元素
- 缺点:代码略繁琐,条件写错会越界
性能:直接通过索引访问,速度最快,时间复杂度 O(n)。
2.2 foreach 循环遍历:只管读,不管索引
是什么:编译器自动帮你处理索引和边界,你只需关心「对每个元素做什么」。
怎么用:
using System;
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78, 60, 95 };
// foreach:依次把每个元素「倒」进变量 score,无需关心索引
foreach (int score in scores)
{
Console.WriteLine($"成绩:{score}");
}
// 注意:foreach 里不能通过迭代变量修改原数组
foreach (int score in scores)
{
// score += 5; // 编译错误!score 是只读的
}
}
}逐行拆解:
foreach (int score in scores)读作「对 scores 里的每一个 int,叫它 score,执行一遍循环体」- 编译器自动帮你处理索引和边界,永远不会越界
- 循环变量
score是每个元素的副本(拷贝),而且是只读的
为什么 foreach 不能修改元素?
这是新手最常困惑的问题。原因在于值类型 vs 引用类型的区别:
int[] scores = { 85, 92, 78 };
foreach (int score in scores)
{
// score 是 scores[i] 的「副本」,不是 scores[i] 本身
// 就像你复印了一张成绩单,在复印件上改分数,原件不会变
score += 5; // 修改的是副本,原数组不受影响
}底层原理:foreach 在编译时会被编译器转换成类似这样的代码:
// 伪代码:foreach 的实际执行逻辑
int[] temp = scores; // 临时引用指向原数组
for (int i = 0; i < temp.Length; i++)
{
int score = temp[i]; // 把元素「拷贝」到 score
// 你的循环体...
// score 是副本,改它不影响 temp[i]
}对于值类型(int、double 等),score 是元素的副本,修改它不影响原数组。 对于引用类型(string、自定义类等),score 是引用的副本,虽然不能让 score 指向新对象,但可以修改对象的属性:
class Student { public string Name; public int Score; }
Student[] students = { new Student { Name = "张三", Score = 85 } };
foreach (Student s in students)
{
// s = new Student(); // 错误:不能让 s 指向新对象
s.Score += 5; // 正确:可以修改对象的属性
}什么时候用:
- ✅ 只读遍历(不修改元素)
- ✅ 不需要索引
- ✅ 追求代码简洁、安全(不会越界)
优缺点:
- 优点:代码简洁、安全(编译器保证不越界)
- 缺点:拿不到索引、不能修改值类型元素
性能:与 for 循环相当,现代 C# 编译器会优化成类似 for 的代码。
2.3 LINQ 方法遍历:函数式编程的优雅
是什么:LINQ(Language Integrated Query,语言集成查询)是 C# 的查询语法,可以用链式调用的方式处理集合。
怎么用:
using System;
using System.Linq; // 必须引入这个命名空间!
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78, 60, 95 };
// 1. ForEach:对每个元素执行操作(类似 foreach)
scores.ToList().ForEach(score => Console.WriteLine($"成绩:{score}"));
// 2. Select:投影转换(把每个元素变成新形式)
var passed = scores.Select(s => s >= 60 ? "及格" : "不及格");
foreach (var p in passed)
{
Console.WriteLine(p);
}
// 3. Where:筛选(只保留符合条件的元素)
var highScores = scores.Where(s => s >= 90);
foreach (var h in highScores)
{
Console.WriteLine($"高分:{h}");
}
// 4. 聚合操作:Sum、Average、Max、Min
Console.WriteLine($"总分:{scores.Sum()}"); // 输出:410
Console.WriteLine($"平均分:{scores.Average()}"); // 输出:82
Console.WriteLine($"最高分:{scores.Max()}"); // 输出:95
Console.WriteLine($"最低分:{scores.Min()}"); // 输出:60
}
}逐行拆解:
using System.Linq;—— LINQ 方法都在这个命名空间下,必须引入scores.ToList().ForEach(...)——ToList()把数组转成 List,然后调用ForEach方法;score => ...是 Lambda 表达式,读作「对每个 score 做什么」Select(s => s >= 60 ? "及格" : "不及格")—— 把每个分数映射成「及格/不及格」字符串Where(s => s >= 90)—— 筛选出大于等于 90 的元素Sum()、Average()等 —— 直接计算聚合结果,无需手写循环
什么时候用:
- ✅ 需要筛选、转换、聚合等复杂操作
- ✅ 追求代码简洁、可读性强
- ✅ 链式调用(
scores.Where(...).Select(...).Sum())
优缺点:
- 优点:代码极简、功能强大、可读性好
- 缺点:性能略低于手写循环(有额外开销)、调试稍麻烦、需要学习 Lambda 语法
性能对比:
| 遍历方式 | 性能 | 适用场景 |
|---|---|---|
| for 循环 | 最快 | 需要索引、修改元素、极致性能 |
| foreach 循环 | 接近 for | 只读遍历、追求简洁 |
| LINQ 方法 | 略慢(有委托调用开销) | 复杂筛选/转换/聚合、追求可读性 |
实际建议:
- 简单遍历用
foreach - 需要索引或修改用
for - 复杂数据处理用 LINQ
- 性能敏感场景(如游戏开发、高频交易)优先
for
2.4 三种遍历方式对比总结
| 对比项 | for | foreach | LINQ |
|---|---|---|---|
| 能否拿到索引 | ✅ | ❌ | ❌(除非用 Select((x, i) => ...)) |
| 能否修改元素 | ✅ | ❌(值类型) | ❌ |
| 越界风险 | 有 | 无 | 无 |
| 代码简洁度 | 一般 | 简洁 | 极简 |
| 性能 | 最快 | 接近 for | 略慢 |
| 学习成本 | 低 | 低 | 中(需学 Lambda) |
| 适用场景 | 全能 | 只读遍历 | 复杂数据处理 |
三、核心章节 2:常用内置方法全解析
C# 数组自带大量实用方法,按功能分为三类:元素操作、属性查询、数组处理。
3.1 元素操作类
3.1.1 GetValue / SetValue:用方法读写元素
所属命名空间:System(数组基类 Array 的方法)
是什么:通过方法(而非下标)读写数组元素。
参数与返回值:
GetValue(int index):参数是索引,返回值是object类型(所有类型的基类)SetValue(object value, int index):参数是要设置的值和索引,无返回值
怎么用:
using System;
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78 };
// GetValue(索引):读取元素,返回 object
object value = scores.GetValue(1);
Console.WriteLine(value); // 输出:92
// 需要参与 int 运算时,要强制转换
int score = (int)scores.GetValue(1);
Console.WriteLine(score + 10); // 输出:102
// SetValue(值, 索引):写入元素
scores.SetValue(100, 1); // 等价于 scores[1] = 100
Console.WriteLine(scores[1]); // 输出:100
}
}什么时候用:
- 日常开发中几乎不用,直接用下标
[]更简洁高效 - 主要用于不知道数组具体类型的场景(如反射、通用工具方法)
常见坑点:
- 返回
object类型,需要强制转换才能参与运算 - 性能略低于直接用下标(有装箱/拆箱开销)
3.1.2 Clone:复制一个独立的数组
所属命名空间:System
是什么:创建一个新数组,内容与原数组完全相同,但两者互不影响。
参数与返回值:
- 无参数
- 返回值是
object类型,需要强制转换回原数组类型
怎么用:
using System;
class Program
{
static void Main()
{
int[] original = { 1, 2, 3 };
// Clone() 创建独立副本
int[] copy = (int[])original.Clone();
copy[0] = 999; // 修改副本
Console.WriteLine($"原数组[0]:{original[0]}"); // 输出:1(不受影响)
Console.WriteLine($"副本[0]:{copy[0]}"); // 输出:999
// 对比:直接赋值 ≠ 复制!
int[] alias = original; // 只是起了个「别名」,指向同一个数组
alias[1] = 888;
Console.WriteLine($"原数组[1]:{original[1]}"); // 输出:888(被改了!)
}
}什么时候用:
- 需要修改数组但不想影响原数组
- 传递数组给方法,但不想被方法内部修改
常见坑点:
- 返回值是
object,必须强制转换:int[] copy = (int[])original.Clone(); int[] b = a;不是复制,只是别名,改 b 会影响 a
3.1.3 CopyTo:把元素复制到另一个数组
所属命名空间:System
是什么:把当前数组的所有元素复制到另一个已存在的数组中。
参数与返回值:
CopyTo(Array array, int index):- 参数 1:目标数组
- 参数 2:目标数组的起始索引(从哪个位置开始放)
- 无返回值
怎么用:
using System;
class Program
{
static void Main()
{
int[] source = { 1, 2, 3 };
int[] target = new int[5]; // 目标数组必须已存在且足够大
// 把 source 的所有元素复制到 target,从索引 0 开始放
source.CopyTo(target, 0);
foreach (int n in target)
{
Console.Write(n + " "); // 输出:1 2 3 0 0
}
Console.WriteLine();
// 从索引 2 开始放
int[] target2 = new int[5];
source.CopyTo(target2, 2);
foreach (int n in target2)
{
Console.Write(n + " "); // 输出:0 0 1 2 3
}
}
}什么时候用:
- 合并数组(把多个小数组复制到一个大数组)
- 扩容(新建大数组,把旧数据 CopyTo 过去)
常见坑点:
- 目标数组必须已初始化(不能是
null) - 目标数组必须足够大,否则抛出
ArgumentException - 是浅拷贝:如果元素是引用类型,复制的是引用,不是对象本身
3.2 属性类
3.2.1 Length:数组长度
所属命名空间:System
是什么:返回数组的元素总个数。
怎么用:
int[] scores = { 85, 92, 78, 60, 95 };
Console.WriteLine(scores.Length); // 输出:5
Console.WriteLine(scores[scores.Length - 1]); // 输出:95(最后一个元素)注意:
Length是属性不是方法,后面不加括号- 最后一个元素的索引永远是
Length - 1
3.2.2 Rank:数组的维度数
所属命名空间:System
是什么:返回数组的维度(一维数组返回 1,二维返回 2,以此类推)。
怎么用:
int[] oneDim = { 1, 2, 3 };
int[,] twoDim = new int[3, 4];
Console.WriteLine(oneDim.Rank); // 输出:1(一维数组)
Console.WriteLine(twoDim.Rank); // 输出:2(二维数组)什么时候用:
- 编写通用方法时,判断传入的是几维数组
- 日常开发中用得较少
3.3 数组处理类(Array 类的静态方法)
这些方法都是 Array 类的静态方法,调用时要写 Array.方法名(...)。
3.3.1 Sort:排序(会直接修改原数组!)
所属命名空间:System
是什么:对数组元素进行升序排序。
参数与返回值:
Array.Sort(Array array):参数是要排序的数组- 无返回值,直接修改原数组
怎么用:
using System;
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78, 60, 95 };
Console.WriteLine("排序前:");
foreach (int s in scores)
{
Console.Write(s + " "); // 输出:85 92 78 60 95
}
// Sort:升序排序,直接修改原数组
Array.Sort(scores);
Console.WriteLine("\n排序后:");
foreach (int s in scores)
{
Console.Write(s + " "); // 输出:60 78 85 92 95
}
// 字符串数组也能排序(按字母顺序)
string[] names = { "张三", "李四", "王五" };
Array.Sort(names);
foreach (string n in names)
{
Console.WriteLine(n); // 按拼音/Unicode 排序
}
}
}什么时候用:
- 需要按大小/字母顺序排列元素
- 配合
BinarySearch(二分查找)前必须排序
常见坑点:
- ⚠️ 直接修改原数组,不会返回新数组!
- 如果想保留原数组,先 Clone 一份再排序:csharp
int[] sorted = (int[])original.Clone(); Array.Sort(sorted); - 默认是升序,想降序可以排序后 Reverse,或用
Array.Sort(scores, (a, b) => b.CompareTo(a))
3.3.2 Reverse:反转数组(也会修改原数组!)
所属命名空间:System
是什么:把数组元素的顺序颠倒过来。
参数与返回值:
Array.Reverse(Array array):参数是要反转的数组- 无返回值,直接修改原数组
怎么用:
int[] nums = { 1, 2, 3, 4, 5 };
Array.Reverse(nums);
foreach (int n in nums)
{
Console.Write(n + " "); // 输出:5 4 3 2 1
}什么时候用:
- 需要逆序遍历(配合 for 循环更灵活)
- 实现「后进先出」的效果
常见坑点:
- ⚠️ 直接修改原数组
- 想保留原数组,先 Clone
3.3.3 Find:查找第一个符合条件的元素
所属命名空间:System
是什么:返回第一个满足条件的元素,找不到则返回默认值(int 是 0,string 是 null)。
参数与返回值:
Array.Find<T>(T[] array, Predicate<T> match):- 参数 1:要查找的数组
- 参数 2:条件(Lambda 表达式)
- 返回值:找到的元素或默认值
怎么用:
using System;
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78, 60, 95 };
// 找第一个大于 90 的分数
int high = Array.Find(scores, s => s > 90);
Console.WriteLine(high); // 输出:92
// 找不到时返回默认值(int 是 0)
int notFound = Array.Find(scores, s => s > 100);
Console.WriteLine(notFound); // 输出:0(不是 -1!)
// 字符串数组:找不到返回 null
string[] names = { "张三", "李四" };
string result = Array.Find(names, n => n.StartsWith("王"));
Console.WriteLine(result == null ? "没找到" : result); // 输出:没找到
}
}什么时候用:
- 只需要第一个匹配的元素
- 不确定是否存在时,配合默认值判断
常见坑点:
- 找不到时返回默认值(int 是 0,不是 -1!),容易和「找到 0」混淆
- 更安全的做法是用
FindIndex或Exists先判断是否存在
3.3.4 FindAll:查找所有符合条件的元素
所属命名空间:System
是什么:返回所有满足条件的元素,组成一个新数组。
参数与返回值:
Array.FindAll<T>(T[] array, Predicate<T> match):参数同 Find- 返回值:新数组(找不到时返回空数组,不是 null)
怎么用:
int[] scores = { 85, 92, 78, 60, 95, 58 };
// 找所有及格的分数
int[] passed = Array.FindAll(scores, s => s >= 60);
foreach (int p in passed)
{
Console.Write(p + " "); // 输出:85 92 78 60 95
}
Console.WriteLine($"\n共找到 {passed.Length} 个"); // 输出:5什么时候用:
- 需要筛选出所有符合条件的元素
- 比手写「两遍遍历」简洁得多
常见坑点:
- 找不到时返回空数组(
Length = 0),不是null,可以直接 foreach 不会报错
3.3.5 IndexOf / LastIndexOf:查找元素的索引
所属命名空间:System
是什么:
IndexOf:返回第一个匹配元素的索引LastIndexOf:返回最后一个匹配元素的索引- 找不到时返回 -1
参数与返回值:
Array.IndexOf(Array array, object value):- 参数 1:数组
- 参数 2:要查找的值
- 返回值:索引(int),找不到返回 -1
怎么用:
int[] scores = { 85, 92, 78, 92, 60 };
// IndexOf:找第一个 92 的索引
int firstIndex = Array.IndexOf(scores, 92);
Console.WriteLine(firstIndex); // 输出:1
// LastIndexOf:找最后一个 92 的索引
int lastIndex = Array.LastIndexOf(scores, 92);
Console.WriteLine(lastIndex); // 输出:3
// 找不到时返回 -1
int notFound = Array.IndexOf(scores, 100);
Console.WriteLine(notFound); // 输出:-1什么时候用:
- 需要知道元素的位置(索引)
- 判断元素是否存在(
if (index != -1))
常见坑点:
- 找不到返回 -1,不是 0!要判断
if (index != -1) - 对于引用类型,比较的是引用是否相同,不是内容(除非重写了 Equals)
3.3.6 Contains:判断元素是否存在
所属命名空间:System.Linq(LINQ 扩展方法)
是什么:判断数组中是否包含某个元素,返回 true 或 false。
参数与返回值:
array.Contains<T>(T value):参数是要查找的值- 返回值:
bool
怎么用:
using System;
using System.Linq; // 必须引入!
class Program
{
static void Main()
{
int[] scores = { 85, 92, 78, 60, 95 };
// Contains:判断是否包含
bool has90 = scores.Contains(90);
Console.WriteLine(has90); // 输出:False
bool has92 = scores.Contains(92);
Console.WriteLine(has92); // 输出:True
// 常用于条件判断
if (scores.Contains(100))
{
Console.WriteLine("有满分!");
}
else
{
Console.WriteLine("没有满分");
}
}
}什么时候用:
- 只需要判断「有没有」,不关心位置
- 代码比
IndexOf(...) != -1更直观
常见坑点:
- 必须引入
System.Linq命名空间 - 性能:内部是线性查找,时间复杂度 O(n),大数据量时考虑
HashSet
3.4 方法速查表
| 方法 | 作用 | 是否修改原数组 | 返回值 | 常见坑点 |
|---|---|---|---|---|
GetValue(i) | 读取元素 | 否 | object | 需强制转换 |
SetValue(v, i) | 写入元素 | 是 | void | 性能略低 |
Clone() | 复制数组 | 否 | object | 需强制转换 |
CopyTo(dest, i) | 复制到另一数组 | 否(改目标数组) | void | 目标数组必须已存在且足够大 |
Length | 长度 | - | int | 是属性不是方法 |
Rank | 维度数 | - | int | 一维数组返回 1 |
Sort(arr) | 升序排序 | 是 | void | 直接改原数组 |
Reverse(arr) | 反转 | 是 | void | 直接改原数组 |
Find(arr, 条件) | 找第一个 | 否 | 元素或默认值 | 找不到返回默认值(int 是 0) |
FindAll(arr, 条件) | 找所有 | 否 | 新数组 | 找不到返回空数组 |
IndexOf(arr, val) | 找索引 | 否 | int 或 -1 | 找不到返回 -1 |
Contains(val) | 判断存在 | 否 | bool | 需 using System.Linq |
四、实战章节:综合运用遍历与方法
4.1 案例一:学生成绩统计系统
需求:统计班级成绩,输出平均分、最高分、最低分、及格率,并找出所有优秀(≥90)的学生。
using System;
using System.Linq;
class Program
{
static void Main()
{
// 学生姓名和成绩
string[] names = { "张三", "李四", "王五", "赵六", "钱七" };
int[] scores = { 85, 92, 78, 90, 58 };
// 1. 基础统计(用 LINQ)
Console.WriteLine("===== 成绩统计 =====");
Console.WriteLine($"总人数:{scores.Length}");
Console.WriteLine($"平均分:{scores.Average():F1}"); // F1 保留 1 位小数
Console.WriteLine($"最高分:{scores.Max()}");
Console.WriteLine($"最低分:{scores.Min()}");
// 2. 及格率(≥60 分)
int passCount = scores.Count(s => s >= 60);
double passRate = (double)passCount / scores.Length * 100;
Console.WriteLine($"及格率:{passRate:F1}%");
// 3. 找出所有优秀(≥90)的学生
Console.WriteLine("\n===== 优秀学生 =====");
for (int i = 0; i < scores.Length; i++)
{
if (scores[i] >= 90)
{
Console.WriteLine($"{names[i]}:{scores[i]} 分");
}
}
// 4. 成绩排名(降序)
Console.WriteLine("\n===== 成绩排名 =====");
// 把姓名和成绩打包成元组,按成绩降序排序
var ranked = names.Zip(scores, (name, score) => (name, score))
.OrderByDescending(x => x.score)
.ToArray();
int rank = 1;
foreach (var (name, score) in ranked)
{
Console.WriteLine($"第 {rank} 名:{name},{score} 分");
rank++;
}
}
}输出:
===== 成绩统计 =====
总人数:5
平均分:80.6
最高分:92
最低分:58
及格率:80.0%
===== 优秀学生 =====
李四:92 分
赵六:90 分
===== 成绩排名 =====
第 1 名:李四,92 分
第 2 名:赵六,90 分
第 3 名:张三,85 分
第 4 名:王五,78 分
第 5 名:钱七,58 分核心思路拆解:
- LINQ 聚合:
Average()、Max()、Min()一行搞定统计 - 条件计数:
Count(s => s >= 60)统计满足条件的元素个数 - Zip 打包:把两个数组按位置合并成元组序列
- OrderByDescending:按成绩降序排序
- 元组解构:
foreach (var (name, score) in ranked)同时取出姓名和成绩
4.2 案例二:商品价格筛选与排序
需求:从商品列表中筛选出价格 ≥50 的商品,按价格升序排列,并计算平均价格。
using System;
using System.Linq;
class Program
{
static void Main()
{
// 商品名称和价格
string[] products = { "鼠标", "键盘", "显示器", "耳机", "音箱" };
double[] prices = { 49.9, 129.0, 899.0, 199.0, 45.5 };
// 1. 筛选出价格 ≥50 的商品
Console.WriteLine("===== 价格 ≥50 的商品 =====");
var filtered = products.Zip(prices, (name, price) => (name, price))
.Where(x => x.price >= 50)
.OrderBy(x => x.price) // 按价格升序
.ToArray();
foreach (var (name, price) in filtered)
{
Console.WriteLine($"{name}:¥{price:F2}"); // F2 保留 2 位小数
}
// 2. 计算筛选后的平均价格
double avgPrice = filtered.Average(x => x.price);
Console.WriteLine($"\n平均价格:¥{avgPrice:F2}");
// 3. 找出最贵的商品
var mostExpensive = filtered.OrderByDescending(x => x.price).First();
Console.WriteLine($"最贵的商品:{mostExpensive.name},¥{mostExpensive.price:F2}");
// 4. 传统写法对比(不用 LINQ)
Console.WriteLine("\n===== 传统写法(for 循环) =====");
int count = 0;
// 第一遍:数数
for (int i = 0; i < prices.Length; i++)
{
if (prices[i] >= 50)
{
count++;
}
}
// 第二遍:装数据
string[] filteredNames = new string[count];
double[] filteredPrices = new double[count];
int index = 0;
for (int i = 0; i < prices.Length; i++)
{
if (prices[i] >= 50)
{
filteredNames[index] = products[i];
filteredPrices[index] = prices[i];
index++;
}
}
// 排序(冒泡排序,演示用)
for (int i = 0; i < filteredPrices.Length - 1; i++)
{
for (int j = 0; j < filteredPrices.Length - 1 - i; j++)
{
if (filteredPrices[j] > filteredPrices[j + 1])
{
// 交换价格
double tempPrice = filteredPrices[j];
filteredPrices[j] = filteredPrices[j + 1];
filteredPrices[j + 1] = tempPrice;
// 同步交换名称
string tempName = filteredNames[j];
filteredNames[j] = filteredNames[j + 1];
filteredNames[j + 1] = tempName;
}
}
}
for (int i = 0; i < filteredNames.Length; i++)
{
Console.WriteLine($"{filteredNames[i]}:¥{filteredPrices[i]:F2}");
}
}
}输出:
===== 价格 ≥50 的商品 =====
键盘:¥129.00
耳机:¥199.00
显示器:¥899.00
平均价格:¥409.00
最贵的商品:显示器,¥899.00
===== 传统写法(for 循环) =====
键盘:¥129.00
耳机:¥199.00
显示器:¥899.00核心思路拆解:
- LINQ 链式调用:
Zip打包 →Where筛选 →OrderBy排序,一气呵成 - 传统两遍遍历:第一遍数数确定长度,第二遍装数据
- 冒泡排序:演示如何手写排序(实际开发中用
Array.Sort或 LINQ) - 同步交换:排序时价格和名称要一起交换,否则数据错位
五、常见问题解答(FAQ)
Q1:foreach 循环里为什么不能修改数组元素?
A:因为 foreach 的循环变量是元素的副本(拷贝),不是元素本身。
int[] scores = { 85, 92, 78 };
foreach (int score in scores)
{
// score 是 scores[i] 的副本,就像复印件
score += 5; // 编译错误!score 是只读的
}解决方法:用 for 循环通过索引修改:
for (int i = 0; i < scores.Length; i++)
{
scores[i] += 5; // 正确:通过索引直接修改原数组
}Q2:Sort 和 Reverse 会直接修改原数组吗?
A:会! 这两个方法都是「原地操作」,直接修改传入的数组,不返回新数组。
int[] scores = { 85, 92, 78 };
Array.Sort(scores);
// scores 现在变成 { 78, 85, 92 },原顺序被覆盖如果想保留原数组:
int[] original = { 85, 92, 78 };
int[] sorted = (int[])original.Clone(); // 先复制
Array.Sort(sorted); // 再排序副本
// original 不变,sorted 是排序后的Q3:Find 找不到时返回什么?
A:返回默认值,不是 -1!
- int 数组:返回 0
- string 数组:返回 null
- bool 数组:返回 false
int[] scores = { 85, 92, 78 };
int result = Array.Find(scores, s => s > 100);
Console.WriteLine(result); // 输出:0(不是 -1!)容易混淆的场景:如果数组里本来就有 0,你无法区分是「找到了 0」还是「没找到」。
更安全的做法:
int index = Array.FindIndex(scores, s => s > 100);
if (index != -1)
{
Console.WriteLine($"找到了,索引是 {index},值是 {scores[index]}");
}
else
{
Console.WriteLine("没找到");
}Q4:IndexOf 和 Contains 有什么区别?
A:
IndexOf:返回元素的索引(找不到返回 -1)Contains:返回是否存在(true/false)
int[] scores = { 85, 92, 78 };
int index = Array.IndexOf(scores, 92);
if (index != -1)
{
Console.WriteLine($"找到了,索引是 {index}"); // 输出:找到了,索引是 1
}
bool exists = scores.Contains(92);
if (exists)
{
Console.WriteLine("存在"); // 输出:存在
}什么时候用哪个:
- 需要知道位置 →
IndexOf - 只需要判断有没有 →
Contains(代码更直观)
Q5:LINQ 性能比 for 循环差很多吗?
A:有差距,但不大,大多数场景可以忽略。
测试数据(100 万元素的数组,求和):
| 方式 | 耗时 |
|---|---|
| for 循环 | ~1ms |
| foreach 循环 | ~1ms |
| LINQ Sum() | ~2ms |
结论:
- 日常开发(数据处理、业务逻辑):用 LINQ,代码简洁更重要
- 性能敏感场景(游戏、高频交易、大数据量):用 for 循环
Q6:Clone 和直接赋值有什么区别?
A:天壤之别!
int[] a = { 1, 2, 3 };
// 直接赋值:只是别名,指向同一个数组
int[] b = a;
b[0] = 999;
Console.WriteLine(a[0]); // 输出:999(a 也被改了!)
// Clone:创建独立副本
int[] c = (int[])a.Clone();
c[0] = 888;
Console.WriteLine(a[0]); // 输出:999(不受影响)
Console.WriteLine(c[0]); // 输出:888记忆口诀:
=是「起别名」,改一个全都变Clone()是「复印」,改副本不影响原件
Q7:CopyTo 和 Clone 有什么区别?
A:
Clone():创建新数组,返回新数组的引用CopyTo(dest, index):把元素复制到已存在的数组中
int[] source = { 1, 2, 3 };
// Clone:创建新数组
int[] copy1 = (int[])source.Clone();
// CopyTo:复制到已存在的数组
int[] copy2 = new int[5]; // 必须先创建
source.CopyTo(copy2, 0); // copy2 现在是 { 1, 2, 3, 0, 0 }什么时候用哪个:
- 需要一个完整的副本 →
Clone() - 合并多个数组、扩容 →
CopyTo()
六、知识点总结
6.1 遍历方式
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| for | 能拿索引、能改元素、性能最好 | 代码繁琐、有越界风险 | 需要索引/修改、极致性能 |
| foreach | 简洁、安全(不越界) | 不能拿索引、不能改值类型元素 | 只读遍历 |
| LINQ | 极简、功能强大、可读性好 | 性能略低、需学 Lambda | 复杂筛选/转换/聚合 |
6.2 常用方法
元素操作:
GetValue/SetValue:用方法读写元素(日常少用)Clone():创建独立副本(想改数组又不想影响原数组时用)CopyTo(dest, i):复制到已存在的数组(合并、扩容用)
属性:
Length:长度(属性,不加括号)Rank:维度数(一维数组返回 1)
数组处理:
Sort(arr):升序排序(直接改原数组)Reverse(arr):反转(直接改原数组)Find(arr, 条件):找第一个(找不到返回默认值)FindAll(arr, 条件):找所有(找不到返回空数组)IndexOf(arr, val):找索引(找不到返回 -1)Contains(val):判断存在(需 using System.Linq)
6.3 核心原则
- 只读遍历用 foreach,需要索引/修改用 for,复杂处理用 LINQ
- Sort 和 Reverse 会改原数组,想保留原数组先 Clone
- Find 找不到返回默认值(int 是 0),不是 -1
=是别名,Clone 才是复制- 性能敏感用 for,代码简洁用 LINQ
七、入门练习题
遍历练习:定义数组
{12, 45, 7, 89, 23, 56},分别用 for、foreach、LINQ 三种方式遍历并输出每个元素。修改练习:用 for 循环把数组
{1, 2, 3, 4, 5}中的每个元素都乘以 2,输出结果。查找练习:在数组
{34, 67, 12, 90, 45, 67}中:- 用
IndexOf找第一个 67 的索引 - 用
LastIndexOf找最后一个 67 的索引 - 用
Find找第一个大于 60 的元素 - 用
FindAll找所有大于 60 的元素
- 用
排序练习:定义数组
{85, 42, 91, 38, 76},分别输出:- 升序排序后的数组
- 降序排列后的数组(提示:Sort 后 Reverse)
- 原数组保持不变(提示:先 Clone)
统计练习:统计数组
{85, 42, 91, 38, 76, 55, 60, 88}中:- 及格(≥60)的人数
- 优秀(≥85)的人数
- 平均分
综合练习:定义商品名称数组
{"苹果", "香蕉", "橙子", "西瓜"}和价格数组{5.5, 3.0, 8.0, 12.5},完成:- 找出价格最高的商品名称和价格
- 筛选出价格 ≥6 的商品,按价格升序输出
- 计算所有商品的平均价格
思考题:下面这段代码输出什么?为什么?
int[] a = { 3, 1, 2 };
int[] b = a;
Array.Sort(b);
Console.WriteLine(a[0]);点击查看第 7 题答案
输出 1。
因为 b = a 是别名赋值,a 和 b 指向同一个数组。Array.Sort(b) 直接修改了这个数组,所以 a 看到的内容也变了,排序后第一个元素是 1。
如果想让 a 保持不变,应该:
int[] b = (int[])a.Clone();
Array.Sort(b);掌握了数组的遍历和方法,你已经能处理大部分一维数据了。下一篇我们将学习数组的初始化和默认值,深入了解各种数据类型在数组中的默认行为,以及如何避免常见的空引用陷阱。