CSharp 多维数组的遍历方式与常用方法:玩转你的「Excel 表格」
引言:从翻箱倒柜到整面柜墙
上一篇我们学会了怎么建多维数组——创建二维表格、三维仓库、交错货架。但现实开发中,你很少只操作单个格子,更多时候需要:
- 批量查看:把整张成绩单、整个仓库的库存都打印出来
- 批量修改:给所有学生的某门课加 5 分、给整个仓库补货
- 批量统计:计算每门课的平均分、找出库存最多的货位
- 批量处理:矩阵转置、数据筛选、多维聚合
这就像你有一整面储物柜墙,现在要逐排逐列检查、整理、统计。怎么高效地做这些事?C# 提供了 4 种遍历方式和一整套核心内置方法,本文带你一一掌握。
一、基础铺垫:多维数组核心概念回顾
1.1 什么是多维数组(30 秒回顾)
多维数组 = 需要两个或更多个索引才能定位元素的数组。索引的个数称为维度(Rank)。
- 二维数组:
int[,],像 Excel 表格,用[行, 列]定位 - 三维数组:
int[,,],像一摞叠起来的表格,用[层, 行, 列]定位 - 交错数组:
int[][],「数组的数组」,每行长度可以不同
// 二维数组:4 个学生 × 3 门课
int[,] scores = {
{ 90, 85, 78 },
{ 88, 92, 60 },
{ 76, 81, 95 },
{ 65, 70, 72 }
};
// 交错数组:3 个班,人数不同
int[][] classes = {
new[] { 90, 85 },
new[] { 88, 92, 60, 71 },
new[] { 76 }
};关键区别:
| 对比项 | 多维数组 int[,] | 交错数组 int[][] |
|---|---|---|
| 内存结构 | 一整块连续内存 | 外层存引用,内层分散 |
| 每行长度 | 必须相同(矩形) | 可以不同(锯齿) |
| 访问语法 | arr[i, j](逗号) | arr[i][j](双括号) |
| 适用场景 | 规则表格数据 | 不规则分组数据 |
1.2 为什么要遍历
想象你是教务主任,手里拿着 4 个班的成绩表。你会一张一张翻看,而不是只看第一张。遍历就是「挨个访问」数组里的每个元素,是批量处理多维数据的第一步。
二、核心内置方法全解析
C# 的 Array 类为多维数组提供了一套核心方法,掌握它们才能高效遍历和操作。
2.1 GetLength:获取指定维度的长度
所属命名空间:System(Array 类的实例方法)
是什么:返回数组某一维的长度(元素个数)。
参数与返回值:
GetLength(int dimension):- 参数:维度索引(从 0 开始,0 = 第一维,1 = 第二维,以此类推)
- 返回值:
int,该维度的长度
怎么用:
using System;
class Program
{
static void Main()
{
// 4 行 3 列的二维数组
int[,] scores = new int[4, 3];
// GetLength(0):第 0 维(行)的长度
Console.WriteLine(scores.GetLength(0)); // 输出:4
// GetLength(1):第 1 维(列)的长度
Console.WriteLine(scores.GetLength(1)); // 输出:3
// 三维数组:2 层 × 3 行 × 4 列
int[,,] cube = new int[2, 3, 4];
Console.WriteLine(cube.GetLength(0)); // 输出:2(层数)
Console.WriteLine(cube.GetLength(1)); // 输出:3(行数)
Console.WriteLine(cube.GetLength(2)); // 输出:4(列数)
}
}什么时候用:
- 遍历多维数组时,控制每层循环的上界
- 不知道数组具体维度长度时动态获取
常见坑点:
- 参数是维度索引,不是长度!
GetLength(0)表示「第 0 维有多长」,不是「数组有几个维度」 - 维度索引从 0 开始,传 2 给二维数组会抛
IndexOutOfRangeException
2.2 GetLowerBound / GetUpperBound:获取维度边界
所属命名空间:System
是什么:
GetLowerBound(int dimension):返回指定维度的下界(最小索引)GetUpperBound(int dimension):返回指定维度的上界(最大索引)
参数与返回值:
- 参数:维度索引(从 0 开始)
- 返回值:
int,边界值
怎么用:
using System;
class Program
{
static void Main()
{
int[,] scores = new int[4, 3];
// 下界:C# 数组默认从 0 开始
Console.WriteLine(scores.GetLowerBound(0)); // 输出:0(行下界)
Console.WriteLine(scores.GetLowerBound(1)); // 输出:0(列下界)
// 上界:长度 - 1
Console.WriteLine(scores.GetUpperBound(0)); // 输出:3(行上界 = 4-1)
Console.WriteLine(scores.GetUpperBound(1)); // 输出:2(列上界 = 3-1)
}
}什么时候用:
- 日常开发中极少用,因为 C# 数组默认下界就是 0,上界就是
GetLength(dim) - 1 - 主要用于与旧式 COM 组件或非零基数组交互的场景
常见坑点:
- 不要误以为
GetUpperBound返回长度!它返回的是最大索引,不是元素个数 - 对于标准 C# 数组,
GetUpperBound(dim)永远等于GetLength(dim) - 1
2.3 Rank:获取维度数
所属命名空间:System
是什么:返回数组的维度个数(几维数组)。
参数与返回值:
- 无参数(是属性,不是方法)
- 返回值:
int,维度数
怎么用:
int[] oneDim = new int[5];
int[,] twoDim = new int[3, 4];
int[,,] threeDim = new int[2, 3, 4];
Console.WriteLine(oneDim.Rank); // 输出:1
Console.WriteLine(twoDim.Rank); // 输出:2
Console.WriteLine(threeDim.Rank); // 输出:3什么时候用:
- 编写通用方法时,判断传入的是几维数组
- 反射或动态处理未知维度的数组
常见坑点:
Rank是属性,后面不加括号!写成Rank()会编译报错- 与
Length的区别:Rank是「几维」,Length是「总共多少个元素」
2.4 Length:获取元素总数
所属命名空间:System
是什么:返回数组中所有维度的元素总个数。
参数与返回值:
- 无参数(是属性)
- 返回值:
int,元素总数
怎么用:
int[,] scores = new int[4, 3]; // 4 行 3 列
Console.WriteLine(scores.Length); // 输出:12(4×3)
Console.WriteLine(scores.Rank); // 输出:2(二维)
Console.WriteLine(scores.GetLength(0)); // 输出:4(行数)
Console.WriteLine(scores.GetLength(1)); // 输出:3(列数)什么时候用:
- 需要知道数组总共能装多少个元素
- 与
Rank、GetLength配合验证数组结构
常见坑点:
Length是总元素数,不是某一维的长度!- 对于
int[4, 3],Length = 12,GetLength(0) = 4,GetLength(1) = 3,三者完全不同
2.5 GetValue / SetValue:用方法读写元素
所属命名空间:System
是什么:通过方法(而非下标)读写多维数组的元素。
参数与返回值:
GetValue(params int[] indices):- 参数:各维度的索引(可变参数,如
GetValue(1, 2)) - 返回值:
object类型
- 参数:各维度的索引(可变参数,如
SetValue(object value, params int[] indices):- 参数 1:要设置的值
- 参数 2 起:各维度的索引
- 无返回值
怎么用:
using System;
class Program
{
static void Main()
{
int[,] scores = {
{ 90, 85, 78 },
{ 88, 92, 60 }
};
// GetValue(行, 列):读取元素
object val = scores.GetValue(1, 2);
Console.WriteLine(val); // 输出:60
// 参与运算需要强制转换
int score = (int)scores.GetValue(1, 2);
Console.WriteLine(score + 10); // 输出:70
// SetValue(值, 行, 列):写入元素
scores.SetValue(100, 1, 2); // 等价于 scores[1, 2] = 100
Console.WriteLine(scores[1, 2]); // 输出:100
}
}什么时候用:
- 日常开发几乎不用,直接用下标
arr[i, j]更简洁高效 - 主要用于反射或通用工具方法(不知道数组具体类型和维度时)
常见坑点:
- 返回
object类型,需要强制转换 - 索引个数必须与数组维度一致,否则抛
ArgumentException - 性能略低于直接用下标(有装箱/拆箱和参数数组开销)
2.6 Clone:复制多维数组
所属命名空间:System
是什么:创建多维数组的完整副本。
参数与返回值:
- 无参数
- 返回值:
object,需强制转换
怎么用:
int[,] original = { { 1, 2 }, { 3, 4 } };
// Clone() 完整复制(多维数组是连续内存,Clone 就是深拷贝)
int[,] copy = (int[,])original.Clone();
copy[0, 0] = 999;
Console.WriteLine(original[0, 0]); // 输出:1(不受影响)
Console.WriteLine(copy[0, 0]); // 输出:999关键区别:
int[,]的Clone()是深拷贝:整块连续内存完整复制,新旧数组完全独立int[][]的Clone()是浅拷贝:只复制外层数组,内层数组引用共享(详见 Basic23 的误区二)
2.7 方法速查表
| 方法/属性 | 作用 | 参数 | 返回值 | 常见坑点 |
|---|---|---|---|---|
GetLength(dim) | 获取指定维度长度 | 维度索引 | int | 参数是维度索引,不是长度 |
GetLowerBound(dim) | 获取维度下界 | 维度索引 | int | 日常极少用,默认是 0 |
GetUpperBound(dim) | 获取维度上界 | 维度索引 | int | 是最大索引,不是长度 |
Rank | 获取维度数 | 无 | int | 是属性,不加括号 |
Length | 获取元素总数 | 无 | int | 是总元素数,不是某维长度 |
GetValue(i, j) | 读取元素 | 各维度索引 | object | 需强制转换,性能略低 |
SetValue(v, i, j) | 写入元素 | 值+各维度索引 | void | 索引个数必须与维度一致 |
Clone() | 复制数组 | 无 | object | [,] 深拷贝,[][] 浅拷贝 |
三、四种遍历方式全解析
遍历多维数组就是把每个元素挨个访问一遍。C# 提供 4 种主流方案,各有适用场景。
3.1 方式一:嵌套 for 循环(主力方案)
是什么:外层循环走第一维,内层循环走第二维,逐行逐列访问。
怎么用:
using System;
class Program
{
static void Main()
{
int[,] scores = {
{ 90, 85, 78 },
{ 88, 92, 60 },
{ 76, 81, 95 }
};
// 标准双重循环:外层走行,内层走列
for (int i = 0; i < scores.GetLength(0); i++) // i = 行号
{
for (int j = 0; j < scores.GetLength(1); j++) // j = 列号
{
Console.Write($"{scores[i, j],4}"); // ,4 右对齐占 4 字符
}
Console.WriteLine(); // 每行打印完换行
}
}
}输出:
90 85 78
88 92 60
76 81 95逐行拆解:
i < scores.GetLength(0)—— 外层循环控制行,上界是行数j < scores.GetLength(1)—— 内层循环控制列,上界是列数scores[i, j]—— 通过行号+列号定位元素Console.WriteLine()—— 每打印完一行就换行,形成表格
什么时候用:
- ✅ 需要行号/列号(如「第 2 行第 3 列」)
- ✅ 需要修改元素(如
scores[i, j] += 5) - ✅ 需要按特定顺序访问(如逆序、跳行)
- ✅ 性能敏感场景(游戏、高频计算)
优缺点:
- 优点:功能最全,能拿到索引,能改元素,性能最好
- 缺点:代码略繁琐,条件写错会越界
性能:直接通过索引访问,速度最快,时间复杂度 O(n×m)。
3.2 方式二:foreach 循环(最省心)
是什么:编译器自动帮你处理所有维度的索引,按行优先顺序遍历所有元素。
怎么用:
int[,] scores = {
{ 90, 85, 78 },
{ 88, 92, 60 }
};
// C# 的 foreach 对多维数组会「自动摊平」,按行优先顺序遍历
foreach (int score in scores)
{
Console.Write(score + " ");
}
// 输出:90 85 78 88 92 60逐行拆解:
foreach (int score in scores)读作「对 scores 里的每一个 int,叫它 score,执行一遍循环体」- 编译器自动处理行和列的索引,永远不会越界
- 循环变量
score是每个元素的副本,而且是只读的
什么时候用:
- ✅ 只读遍历(不修改元素)
- ✅ 不需要行号/列号
- ✅ 追求代码简洁、安全
优缺点:
- 优点:代码极简、安全(编译器保证不越界)
- 缺点:拿不到行号列号、不能修改元素(副本机制)
性能:与 for 循环相当,现代 C# 编译器会优化成类似 for 的代码。
交错数组的 foreach:
int[][] classes = {
new[] { 90, 85 },
new[] { 88, 92, 60, 71 }
};
// 外层 foreach 拿到的是「每一行」(int[] 类型)
foreach (int[] classScores in classes)
{
// 内层 foreach 遍历当前行的每个元素
foreach (int score in classScores)
{
Console.Write(score + " ");
}
Console.WriteLine();
}3.3 方式三:for + foreach 混合(灵活实用)
是什么:外层用 for 拿行号,内层用 foreach 遍历当前行,兼顾索引和简洁。
怎么用:
int[,] scores = {
{ 90, 85, 78 },
{ 88, 92, 60 },
{ 76, 81, 95 }
};
// 外层 for:需要行号来显示「第几个学生」
for (int i = 0; i < scores.GetLength(0); i++)
{
Console.Write($"学生 {i + 1}:");
// 内层 foreach:遍历当前行的所有成绩
// 注意:不能直接对 scores[i] 用 foreach,必须先提取行
for (int j = 0; j < scores.GetLength(1); j++)
{
Console.Write($"{scores[i, j]} ");
}
Console.WriteLine();
}交错数组的混合遍历(更自然):
int[][] classes = {
new[] { 90, 85 },
new[] { 88, 92, 60, 71 },
new[] { 76 }
};
// 外层 for:需要班号
for (int i = 0; i < classes.Length; i++)
{
Console.Write($"班级 {i}({classes[i].Length} 人):");
// 内层 foreach:遍历当前班的所有学生
foreach (int score in classes[i])
{
Console.Write(score + " ");
}
Console.WriteLine();
}什么时候用:
- ✅ 需要行号/列号,但内层不需要列号
- ✅ 交错数组遍历(内层长度各不相同)
3.4 方式四:LINQ 查询(函数式优雅)
是什么:用 LINQ 的链式调用处理多维数组,适合复杂筛选、转换、聚合。
怎么用:
using System;
using System.Linq;
class Program
{
static void Main()
{
int[,] scores = {
{ 90, 85, 78 },
{ 88, 92, 60 },
{ 76, 81, 95 }
};
// 1. 多维数组不能直接 LINQ,需要先「摊平」
// Cast<int>() 把多维数组转成 IEnumerable<int>
var allScores = scores.Cast<int>();
// 2. 聚合操作
Console.WriteLine($"总分:{allScores.Sum()}"); // 输出:737
Console.WriteLine($"平均分:{allScores.Average():F1}"); // 输出:81.9
Console.WriteLine($"最高分:{allScores.Max()}"); // 输出:95
Console.WriteLine($"最低分:{allScores.Min()}"); // 输出:60
// 3. 筛选:找出所有及格(≥60)的成绩
var passed = allScores.Where(s => s >= 60);
Console.WriteLine($"及格人数:{passed.Count()}"); // 输出:9
// 4. 转换:给每个成绩加 5 分(生成新序列,不改原数组)
var boosted = allScores.Select(s => s + 5);
Console.WriteLine($"加分后平均分:{boosted.Average():F1}"); // 输出:86.9
}
}逐行拆解:
scores.Cast<int>()—— 多维数组不直接支持 LINQ,先用Cast<T>()转成一维序列Sum()、Average()、Max()、Min()—— 聚合统计Where(s => s >= 60)—— 筛选符合条件的元素Select(s => s + 5)—— 投影转换(生成新序列,不改原数组)
什么时候用:
- ✅ 复杂筛选、转换、聚合
- ✅ 追求代码简洁、可读性强
- ✅ 链式调用(
Where(...).Select(...).Sum())
优缺点:
- 优点:代码极简、功能强大、可读性好
- 缺点:性能略低(有委托调用和装箱开销)、不能直接修改原数组、需要学习 Lambda 语法
交错数组的 LINQ:
int[][] classes = {
new[] { 90, 85 },
new[] { 88, 92, 60, 71 },
new[] { 76 }
};
// 交错数组本身就是「数组的数组」,可以直接 LINQ
var allScores = classes.SelectMany(c => c); // 摊平所有班级
Console.WriteLine($"全校总分:{allScores.Sum()}");
Console.WriteLine($"全校平均分:{allScores.Average():F1}");
// 找出每个班的平均分
var classAverages = classes.Select(c => c.Average());
foreach (var avg in classAverages)
{
Console.WriteLine($"班级平均分:{avg:F1}");
}3.5 四种遍历方式对比总结
| 对比项 | 嵌套 for | foreach | for + foreach | LINQ |
|---|---|---|---|---|
| 能否拿索引 | ✅ | ❌ | ✅(外层) | ❌ |
| 能否改元素 | ✅ | ❌ | ✅(外层 for) | ❌ |
| 越界风险 | 有 | 无 | 有(外层) | 无 |
| 代码简洁度 | 一般 | 极简 | 简洁 | 极简 |
| 性能 | 最快 | 接近 for | 接近 for | 略慢 |
| 学习成本 | 低 | 低 | 低 | 中(需学 Lambda) |
| 适用场景 | 全能 | 只读遍历 | 需要行号 | 复杂数据处理 |
实际建议:
- 简单只读遍历用
foreach - 需要索引或修改用嵌套
for - 交错数组遍历用
for + foreach - 复杂数据处理用 LINQ
- 性能敏感场景(游戏、高频计算)优先嵌套
for
四、实战应用案例
4.1 案例一:矩阵运算——图像处理的基础
需求:实现两个 3×3 矩阵的加法和乘法,输出结果矩阵。
矩阵加法:对应位置元素相加。 矩阵乘法:第 i 行第 j 列 = A 的第 i 行与 B 的第 j 列对应元素相乘后求和。
using System;
class Program
{
static void Main()
{
// 定义两个 3×3 矩阵
int[,] matrixA = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int[,] matrixB = {
{ 9, 8, 7 },
{ 6, 5, 4 },
{ 3, 2, 1 }
};
int rows = matrixA.GetLength(0);
int cols = matrixA.GetLength(1);
// ===== 矩阵加法 =====
int[,] sum = new int[rows, cols];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
sum[i, j] = matrixA[i, j] + matrixB[i, j];
}
}
Console.WriteLine("===== 矩阵加法结果 =====");
PrintMatrix(sum);
// ===== 矩阵乘法 =====
// 规则:result[i,j] = Σ(A[i,k] * B[k,j]),k 从 0 到 cols-1
int[,] product = new int[rows, cols];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
int dotProduct = 0;
for (int k = 0; k < cols; k++)
{
dotProduct += matrixA[i, k] * matrixB[k, j];
}
product[i, j] = dotProduct;
}
}
Console.WriteLine("\n===== 矩阵乘法结果 =====");
PrintMatrix(product);
}
// 封装打印矩阵的方法
static void PrintMatrix(int[,] matrix)
{
for (int i = 0; i < matrix.GetLength(0); i++)
{
for (int j = 0; j < matrix.GetLength(1); j++)
{
Console.Write($"{matrix[i, j],5}"); // 右对齐占 5 字符
}
Console.WriteLine();
}
}
}输出:
===== 矩阵加法结果 =====
10 10 10
10 10 10
10 10 10
===== 矩阵乘法结果 =====
30 24 18
84 69 54
138 114 90核心思路拆解:
- 矩阵加法:双重循环遍历,对应位置相加
- 矩阵乘法:三重循环,最内层计算「行 × 列」的点积
- 封装方法:
PrintMatrix复用打印逻辑,避免重复代码 - 适配逻辑:矩阵的行列结构与二维数组完美对应,机器学习中的张量就是 N 维数组
4.2 案例二:多维数据统计——电商销售分析
需求:统计 3 个店铺、4 个季度的销售额,计算:
- 每个店铺的年销售额
- 每个季度的总销售额
- 全年总销售额
- 找出销售额最高的店铺和季度
using System;
using System.Linq;
class Program
{
static void Main()
{
// 3 个店铺 × 4 个季度的销售额(单位:万元)
int[,] sales = {
{ 120, 150, 180, 200 }, // 店铺 0:四个季度
{ 100, 130, 160, 190 }, // 店铺 1
{ 80, 110, 140, 170 } // 店铺 2
};
string[] shopNames = { "旗舰店", "专卖店", "加盟店" };
string[] quarters = { "Q1", "Q2", "Q3", "Q4" };
int shopCount = sales.GetLength(0);
int quarterCount = sales.GetLength(1);
// ===== 1. 每个店铺的年销售额 =====
Console.WriteLine("===== 各店铺年销售额 =====");
for (int i = 0; i < shopCount; i++)
{
int yearlyTotal = 0;
for (int j = 0; j < quarterCount; j++)
{
yearlyTotal += sales[i, j];
}
Console.WriteLine($"{shopNames[i]}:{yearlyTotal} 万元");
}
// ===== 2. 每个季度的总销售额 =====
Console.WriteLine("\n===== 各季度总销售额 =====");
for (int j = 0; j < quarterCount; j++)
{
int quarterTotal = 0;
for (int i = 0; i < shopCount; i++)
{
quarterTotal += sales[i, j];
}
Console.WriteLine($"{quarters[j]}:{quarterTotal} 万元");
}
// ===== 3. 全年总销售额 =====
int grandTotal = sales.Cast<int>().Sum();
Console.WriteLine($"\n全年总销售额:{grandTotal} 万元");
// ===== 4. 找出销售额最高的店铺和季度 =====
int maxSales = 0;
int maxShop = 0, maxQuarter = 0;
for (int i = 0; i < shopCount; i++)
{
for (int j = 0; j < quarterCount; j++)
{
if (sales[i, j] > maxSales)
{
maxSales = sales[i, j];
maxShop = i;
maxQuarter = j;
}
}
}
Console.WriteLine($"\n最高单季销售额:{shopNames[maxShop]} {quarters[maxQuarter]},{maxSales} 万元");
// ===== 5. LINQ 进阶:按店铺统计平均季度销售额 =====
Console.WriteLine("\n===== 各店铺平均季度销售额(LINQ) =====");
for (int i = 0; i < shopCount; i++)
{
// 提取当前店铺的所有季度数据
var shopSales = Enumerable.Range(0, quarterCount)
.Select(j => sales[i, j]);
Console.WriteLine($"{shopNames[i]}:{shopSales.Average():F1} 万元");
}
}
}输出:
===== 各店铺年销售额 =====
旗舰店:650 万元
专卖店:580 万元
加盟店:500 万元
===== 各季度总销售额 =====
Q1:300 万元
Q2:390 万元
Q3:480 万元
Q4:560 万元
全年总销售额:1730 万元
最高单季销售额:旗舰店 Q4,200 万元
===== 各店铺平均季度销售额(LINQ) =====
旗舰店:162.5 万元
专卖店:145.0 万元
加盟店:125.0 万元核心思路拆解:
- 按行求和:外层固定行,内层累加列 → 店铺年销售额
- 按列求和:外层固定列,内层累加行 → 季度总销售额
- Cast + Sum:LINQ 摊平后聚合 → 全年总销售额
- 双重循环找最大值:记录最大值的位置(行号+列号)
- Enumerable.Range + Select:LINQ 提取某一行数据,计算平均值
五、常见问题与避坑指南
5.1 坑一:混淆 [,] 和 [][] 的声明与访问语法
错误示例:
// 错误 1:把多维数组当交错数组访问
int[,] multi = new int[2, 3];
// multi[0][0] = 1; // 编译错误!int[,] 不能用两对括号
// 错误 2:把交错数组当多维数组访问
int[][] jagged = new int[2][];
jagged[0] = new int[3];
// jagged[0, 0] = 1; // 编译错误!int[][] 不能用逗号修正方案:
// 记忆口诀:声明用什么,访问就用什么
multi[0, 0] = 1; // int[,] → 逗号访问
jagged[0][0] = 1; // int[][] → 双括号访问5.2 坑二:GetLength 参数传反,导致越界
错误示例:
int[,] grid = new int[2, 3]; // 2 行 3 列
// 错误:用列数控制行循环,用行数控制列循环
for (int i = 0; i < grid.GetLength(1); i++) // i 会走到 2,越界!
{
for (int j = 0; j < grid.GetLength(0); j++) // j 会走到 3,越界!
{
Console.WriteLine(grid[i, j]);
}
}修正方案:
// 记忆口诀:GetLength(0) 对应第一个索引,GetLength(1) 对应第二个索引
for (int i = 0; i < grid.GetLength(0); i++) // i 是行,用行数控制
{
for (int j = 0; j < grid.GetLength(1); j++) // j 是列,用列数控制
{
Console.WriteLine(grid[i, j]);
}
}5.3 坑三:foreach 试图修改元素
错误示例:
int[,] scores = { { 90, 85 }, { 88, 92 } };
// 错误:foreach 的循环变量是只读副本
foreach (int score in scores)
{
// score += 5; // 编译错误!score 是只读的
}修正方案:
// 用嵌套 for 通过索引修改
for (int i = 0; i < scores.GetLength(0); i++)
{
for (int j = 0; j < scores.GetLength(1); j++)
{
scores[i, j] += 5; // 正确:通过索引直接修改原数组
}
}5.4 坑四:LINQ 直接对多维数组操作
错误示例:
int[,] scores = { { 90, 85 }, { 88, 92 } };
// 错误:多维数组不直接支持 LINQ
// var sum = scores.Sum(); // 编译错误!
// var max = scores.Max(); // 编译错误!修正方案:
// 先用 Cast<T>() 摊平成一维序列
var allScores = scores.Cast<int>();
var sum = allScores.Sum(); // 正确
var max = allScores.Max(); // 正确5.5 坑五:交错数组忘记 new 内层,或遍历时用固定长度
错误示例:
// 错误 1:忘记 new 内层
int[][] jagged = new int[3][];
// jagged[0][0] = 1; // 运行时异常!jagged[0] 是 null
// 错误 2:遍历时假设每行长度相同
int[][] classes = {
new[] { 90, 85 },
new[] { 88, 92, 60, 71 },
new[] { 76 }
};
for (int i = 0; i < classes.Length; i++)
{
for (int j = 0; j < 4; j++) // 第 0 行只有 2 个,第 2 行只有 1 个!
{
Console.Write(classes[i][j] + " "); // 越界异常!
}
}修正方案:
// 修正 1:内层必须先 new
jagged[0] = new int[3];
jagged[0][0] = 1; // 正确
// 修正 2:内层循环上界用「当前行自己的长度」
for (int i = 0; i < classes.Length; i++)
{
for (int j = 0; j < classes[i].Length; j++) // 关键:classes[i].Length
{
Console.Write(classes[i][j] + " ");
}
Console.WriteLine();
}5.6 坑六:混淆 Length、Rank、GetLength
错误示例:
int[,] arr = new int[4, 3];
// 错误:以为 Length 是行数
// for (int i = 0; i < arr.Length; i++) // i 会走到 11,越界!
// 错误:以为 Rank() 是方法
// Console.WriteLine(arr.Rank()); // 编译错误!Rank 是属性修正方案:
// 正确理解三个概念
Console.WriteLine(arr.Length); // 12(总元素数 = 4×3)
Console.WriteLine(arr.Rank); // 2(维度数)
Console.WriteLine(arr.GetLength(0)); // 4(行数)
Console.WriteLine(arr.GetLength(1)); // 3(列数)
// 遍历用 GetLength,不是 Length
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
// ...
}
}六、知识点总结
6.1 核心内置方法
| 方法/属性 | 作用 | 记忆要点 |
|---|---|---|
GetLength(dim) | 获取指定维度长度 | 参数是维度索引(0=行,1=列) |
GetLowerBound(dim) | 获取维度下界 | 默认是 0,极少用 |
GetUpperBound(dim) | 获取维度上界 | 是最大索引,不是长度 |
Rank | 获取维度数 | 是属性,不加括号 |
Length | 获取元素总数 | 是总个数,不是某维长度 |
GetValue(i, j) | 读取元素 | 返回 object,需强转 |
SetValue(v, i, j) | 写入元素 | 日常少用,性能略低 |
Clone() | 复制数组 | [,] 深拷贝,[][] 浅拷贝 |
6.2 四种遍历方式
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 嵌套 for | 能拿索引、能改元素、性能最好 | 代码繁琐、有越界风险 | 需要索引/修改、极致性能 |
| foreach | 极简、安全(不越界) | 不能拿索引、不能改元素 | 只读遍历 |
| for + foreach | 兼顾索引和简洁 | 外层仍有越界风险 | 交错数组、需要行号 |
| LINQ | 极简、功能强大 | 性能略低、不能直接改数组 | 复杂筛选/转换/聚合 |
6.3 核心原则
- 声明用什么,访问就用什么:
[,]用逗号,[][]用双括号 - GetLength(0) 对应第一个索引,GetLength(1) 对应第二个索引
- foreach 不能改元素,要改就用 for 通过索引
- 多维数组用 LINQ 前先 Cast<T>() 摊平
- 交错数组内层必须先 new,遍历用 classes[i].Length 自适应
七、入门练习题
遍历练习:定义一个 3×3 的二维数组
{ {1,2,3}, {4,5,6}, {7,8,9} },分别用嵌套 for、foreach、LINQ 三种方式输出所有元素。修改练习:用嵌套 for 把二维数组
{ {1,2}, {3,4} }中的每个元素都乘以 2,输出结果。统计练习:有一个 4×3 的成绩数组,计算:
- 每个学生的总分
- 每门课的平均分
- 全班最高分
交错数组练习:定义交错数组
{ new[]{80,90}, new[]{70,85,95}, new[]{60} },找出所有分数中的最高分和最低分。矩阵练习:实现两个 2×2 矩阵的减法,输出结果矩阵。
综合练习:有一个 2×2×2 的三维数组表示「2 个仓库 × 2 个货架 × 2 个货位」,统计:
- 每个仓库的总库存
- 库存最多的货位位置(仓库号、货架号、货位号)
思考题:下面这段代码输出什么?为什么?
int[,] a = { { 1, 2 }, { 3, 4 } };
int[,] b = a;
b[0, 0] = 999;
Console.WriteLine(a[0, 0]);点击查看第 7 题答案
输出 999。
因为 b = a 是别名赋值,a 和 b 指向同一个二维数组(连续内存块)。修改 b[0, 0] 就是修改 a[0, 0]。
如果想让 a 保持不变,应该:
int[,] b = (int[,])a.Clone();
b[0, 0] = 999;掌握了多维数组的遍历和方法,你已经能处理大部分多维数据了。下一篇我们将学习交错数组的定义和基础使用,深入了解这种「数组的数组」与真多维数组的本质区别,以及它在实际开发中的独特价值。