Skip to content

C# 一维数组的遍历方式与常用内置方法:玩转你的「储物柜」 ​

引言:从翻箱倒柜到批量处理 ​

上一篇我们学会了怎么建「储物柜」——创建数组、放数据、取数据。但现实开发中,你很少只操作单个元素,更多时候需要:

  • 批量查看:把全班 50 个学生的成绩都打印出来
  • 批量修改:给所有商品打 8 折
  • 批量统计:计算平均分、找出最高分和最低分
  • 批量处理:排序、筛选、复制

这就像你有一排储物柜,现在要挨个检查、整理、统计。怎么高效地做这些事?C# 提供了三种遍历方式和一大波内置方法,本文带你一一掌握。


一、基础铺垫:回顾一维数组的核心概念 ​

在开始遍历之前,先确保你已经掌握了数组的基础。如果你还不熟悉,建议先阅读上一篇《C# 一维数组的定义和基础使用》。

1.1 什么是数组(30 秒回顾) ​

数组 = 一排固定数量、相同类型的储物柜,通过从 0 开始的索引访问。

csharp
// 创建一个有 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,逐个访问元素。

怎么用:

csharp
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
    }
}

逐行拆解:

  1. int i = 0 —— 索引从 0 开始(第一个元素)
  2. i < scores.Length —— 注意是小于不是小于等于!当 i 等于 Length 时已经越界
  3. i++ —— 每轮结束后索引加 1,走向下一个元素
  4. scores[i] += 5 —— 通过索引直接修改原数组的元素

什么时候用:

  • ✅ 需要索引(如「第 3 个元素」)
  • ✅ 需要修改元素
  • ✅ 需要逆序遍历(for (int i = arr.Length - 1; i >= 0; i--))
  • ✅ 需要跳过某些元素(如每隔一个取一个)

优缺点:

  • 优点:功能最全,能拿到索引,能改元素
  • 缺点:代码略繁琐,条件写错会越界

性能:直接通过索引访问,速度最快,时间复杂度 O(n)。

2.2 foreach 循环遍历:只管读,不管索引 ​

是什么:编译器自动帮你处理索引和边界,你只需关心「对每个元素做什么」。

怎么用:

csharp
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 引用类型的区别:

csharp
int[] scores = { 85, 92, 78 };

foreach (int score in scores)
{
    // score 是 scores[i] 的「副本」,不是 scores[i] 本身
    // 就像你复印了一张成绩单,在复印件上改分数,原件不会变
    score += 5;  // 修改的是副本,原数组不受影响
}

底层原理:foreach 在编译时会被编译器转换成类似这样的代码:

csharp
// 伪代码: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 指向新对象,但可以修改对象的属性:

csharp
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# 的查询语法,可以用链式调用的方式处理集合。

怎么用:

csharp
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 三种遍历方式对比总结 ​

对比项forforeachLINQ
能否拿到索引✅❌❌(除非用 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):参数是要设置的值和索引,无返回值

怎么用:

csharp
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 类型,需要强制转换回原数组类型

怎么用:

csharp
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:目标数组的起始索引(从哪个位置开始放)
  • 无返回值

怎么用:

csharp
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

是什么:返回数组的元素总个数。

怎么用:

csharp
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,以此类推)。

怎么用:

csharp
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):参数是要排序的数组
  • 无返回值,直接修改原数组

怎么用:

csharp
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):参数是要反转的数组
  • 无返回值,直接修改原数组

怎么用:

csharp
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 表达式)
  • 返回值:找到的元素或默认值

怎么用:

csharp
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)

怎么用:

csharp
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

怎么用:

csharp
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

怎么用:

csharp
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)的学生。

csharp
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++;
        }
    }
}

输出:

text
===== 成绩统计 =====
总人数:5
平均分:80.6
最高分:92
最低分:58
及格率:80.0%

===== 优秀学生 =====
李四:92 分
赵六:90 分

===== 成绩排名 =====
第 1 名:李四,92 分
第 2 名:赵六,90 分
第 3 名:张三,85 分
第 4 名:王五,78 分
第 5 名:钱七,58 分

核心思路拆解:

  1. LINQ 聚合:Average()、Max()、Min() 一行搞定统计
  2. 条件计数:Count(s => s >= 60) 统计满足条件的元素个数
  3. Zip 打包:把两个数组按位置合并成元组序列
  4. OrderByDescending:按成绩降序排序
  5. 元组解构:foreach (var (name, score) in ranked) 同时取出姓名和成绩

4.2 案例二:商品价格筛选与排序 ​

需求:从商品列表中筛选出价格 ≥50 的商品,按价格升序排列,并计算平均价格。

csharp
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}");
        }
    }
}

输出:

text
===== 价格 ≥50 的商品 =====
键盘:¥129.00
耳机:¥199.00
显示器:¥899.00

平均价格:¥409.00
最贵的商品:显示器,¥899.00

===== 传统写法(for 循环) =====
键盘:¥129.00
耳机:¥199.00
显示器:¥899.00

核心思路拆解:

  1. LINQ 链式调用:Zip 打包 → Where 筛选 → OrderBy 排序,一气呵成
  2. 传统两遍遍历:第一遍数数确定长度,第二遍装数据
  3. 冒泡排序:演示如何手写排序(实际开发中用 Array.Sort 或 LINQ)
  4. 同步交换:排序时价格和名称要一起交换,否则数据错位

五、常见问题解答(FAQ) ​

Q1:foreach 循环里为什么不能修改数组元素? ​

A:因为 foreach 的循环变量是元素的副本(拷贝),不是元素本身。

csharp
int[] scores = { 85, 92, 78 };

foreach (int score in scores)
{
    // score 是 scores[i] 的副本,就像复印件
    score += 5;  // 编译错误!score 是只读的
}

解决方法:用 for 循环通过索引修改:

csharp
for (int i = 0; i < scores.Length; i++)
{
    scores[i] += 5;  // 正确:通过索引直接修改原数组
}

Q2:Sort 和 Reverse 会直接修改原数组吗? ​

A:会! 这两个方法都是「原地操作」,直接修改传入的数组,不返回新数组。

csharp
int[] scores = { 85, 92, 78 };
Array.Sort(scores);
// scores 现在变成 { 78, 85, 92 },原顺序被覆盖

如果想保留原数组:

csharp
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
csharp
int[] scores = { 85, 92, 78 };
int result = Array.Find(scores, s => s > 100);
Console.WriteLine(result);  // 输出:0(不是 -1!)

容易混淆的场景:如果数组里本来就有 0,你无法区分是「找到了 0」还是「没找到」。

更安全的做法:

csharp
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)
csharp
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:天壤之别!

csharp
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):把元素复制到已存在的数组中
csharp
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 核心原则 ​

  1. 只读遍历用 foreach,需要索引/修改用 for,复杂处理用 LINQ
  2. Sort 和 Reverse 会改原数组,想保留原数组先 Clone
  3. Find 找不到返回默认值(int 是 0),不是 -1
  4. = 是别名,Clone 才是复制
  5. 性能敏感用 for,代码简洁用 LINQ

七、入门练习题 ​

  1. 遍历练习:定义数组 {12, 45, 7, 89, 23, 56},分别用 for、foreach、LINQ 三种方式遍历并输出每个元素。

  2. 修改练习:用 for 循环把数组 {1, 2, 3, 4, 5} 中的每个元素都乘以 2,输出结果。

  3. 查找练习:在数组 {34, 67, 12, 90, 45, 67} 中:

    • 用 IndexOf 找第一个 67 的索引
    • 用 LastIndexOf 找最后一个 67 的索引
    • 用 Find 找第一个大于 60 的元素
    • 用 FindAll 找所有大于 60 的元素
  4. 排序练习:定义数组 {85, 42, 91, 38, 76},分别输出:

    • 升序排序后的数组
    • 降序排列后的数组(提示:Sort 后 Reverse)
    • 原数组保持不变(提示:先 Clone)
  5. 统计练习:统计数组 {85, 42, 91, 38, 76, 55, 60, 88} 中:

    • 及格(≥60)的人数
    • 优秀(≥85)的人数
    • 平均分
  6. 综合练习:定义商品名称数组 {"苹果", "香蕉", "橙子", "西瓜"} 和价格数组 {5.5, 3.0, 8.0, 12.5},完成:

    • 找出价格最高的商品名称和价格
    • 筛选出价格 ≥6 的商品,按价格升序输出
    • 计算所有商品的平均价格
  7. 思考题:下面这段代码输出什么?为什么?

csharp
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 保持不变,应该:

csharp
int[] b = (int[])a.Clone();
Array.Sort(b);

掌握了数组的遍历和方法,你已经能处理大部分一维数据了。下一篇我们将学习数组的初始化和默认值,深入了解各种数据类型在数组中的默认行为,以及如何避免常见的空引用陷阱。