Skip to content

CSharp 多维数组的遍历方式与常用方法:玩转你的「Excel 表格」 ​

引言:从翻箱倒柜到整面柜墙 ​

上一篇我们学会了怎么建多维数组——创建二维表格、三维仓库、交错货架。但现实开发中,你很少只操作单个格子,更多时候需要:

  • 批量查看:把整张成绩单、整个仓库的库存都打印出来
  • 批量修改:给所有学生的某门课加 5 分、给整个仓库补货
  • 批量统计:计算每门课的平均分、找出库存最多的货位
  • 批量处理:矩阵转置、数据筛选、多维聚合

这就像你有一整面储物柜墙,现在要逐排逐列检查、整理、统计。怎么高效地做这些事?C# 提供了 4 种遍历方式和一整套核心内置方法,本文带你一一掌握。


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

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

多维数组 = 需要两个或更多个索引才能定位元素的数组。索引的个数称为维度(Rank)。

  • 二维数组:int[,],像 Excel 表格,用 [行, 列] 定位
  • 三维数组:int[,,],像一摞叠起来的表格,用 [层, 行, 列] 定位
  • 交错数组:int[][],「数组的数组」,每行长度可以不同
csharp
// 二维数组: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,该维度的长度

怎么用:

csharp
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,边界值

怎么用:

csharp
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,维度数

怎么用:

csharp
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,元素总数

怎么用:

csharp
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 起:各维度的索引
    • 无返回值

怎么用:

csharp
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,需强制转换

怎么用:

csharp
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 循环(主力方案) ​

是什么:外层循环走第一维,内层循环走第二维,逐行逐列访问。

怎么用:

csharp
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();   // 每行打印完换行
        }
    }
}

输出:

text
  90  85  78
  88  92  60
  76  81  95

逐行拆解:

  1. i < scores.GetLength(0) —— 外层循环控制行,上界是行数
  2. j < scores.GetLength(1) —— 内层循环控制列,上界是列数
  3. scores[i, j] —— 通过行号+列号定位元素
  4. Console.WriteLine() —— 每打印完一行就换行,形成表格

什么时候用:

  • ✅ 需要行号/列号(如「第 2 行第 3 列」)
  • ✅ 需要修改元素(如 scores[i, j] += 5)
  • ✅ 需要按特定顺序访问(如逆序、跳行)
  • ✅ 性能敏感场景(游戏、高频计算)

优缺点:

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

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

3.2 方式二:foreach 循环(最省心) ​

是什么:编译器自动帮你处理所有维度的索引,按行优先顺序遍历所有元素。

怎么用:

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

csharp
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 遍历当前行,兼顾索引和简洁。

怎么用:

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

交错数组的混合遍历(更自然):

csharp
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 的链式调用处理多维数组,适合复杂筛选、转换、聚合。

怎么用:

csharp
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&lt;int&gt;() —— 多维数组不直接支持 LINQ,先用 Cast&lt;T&gt;() 转成一维序列
  • Sum()、Average()、Max()、Min() —— 聚合统计
  • Where(s => s >= 60) —— 筛选符合条件的元素
  • Select(s => s + 5) —— 投影转换(生成新序列,不改原数组)

什么时候用:

  • ✅ 复杂筛选、转换、聚合
  • ✅ 追求代码简洁、可读性强
  • ✅ 链式调用(Where(...).Select(...).Sum())

优缺点:

  • 优点:代码极简、功能强大、可读性好
  • 缺点:性能略低(有委托调用和装箱开销)、不能直接修改原数组、需要学习 Lambda 语法

交错数组的 LINQ:

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

对比项嵌套 forforeachfor + foreachLINQ
能否拿索引✅❌✅(外层)❌
能否改元素✅❌✅(外层 for)❌
越界风险有无有(外层)无
代码简洁度一般极简简洁极简
性能最快接近 for接近 for略慢
学习成本低低低中(需学 Lambda)
适用场景全能只读遍历需要行号复杂数据处理

实际建议:

  • 简单只读遍历用 foreach
  • 需要索引或修改用嵌套 for
  • 交错数组遍历用 for + foreach
  • 复杂数据处理用 LINQ
  • 性能敏感场景(游戏、高频计算)优先嵌套 for

四、实战应用案例 ​

4.1 案例一:矩阵运算——图像处理的基础 ​

需求:实现两个 3×3 矩阵的加法和乘法,输出结果矩阵。

矩阵加法:对应位置元素相加。 矩阵乘法:第 i 行第 j 列 = A 的第 i 行与 B 的第 j 列对应元素相乘后求和。

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

输出:

text
===== 矩阵加法结果 =====
   10   10   10
   10   10   10
   10   10   10

===== 矩阵乘法结果 =====
   30   24   18
   84   69   54
  138  114   90

核心思路拆解:

  1. 矩阵加法:双重循环遍历,对应位置相加
  2. 矩阵乘法:三重循环,最内层计算「行 × 列」的点积
  3. 封装方法:PrintMatrix 复用打印逻辑,避免重复代码
  4. 适配逻辑:矩阵的行列结构与二维数组完美对应,机器学习中的张量就是 N 维数组

4.2 案例二:多维数据统计——电商销售分析 ​

需求:统计 3 个店铺、4 个季度的销售额,计算:

  • 每个店铺的年销售额
  • 每个季度的总销售额
  • 全年总销售额
  • 找出销售额最高的店铺和季度
csharp
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} 万元");
        }
    }
}

输出:

text
===== 各店铺年销售额 =====
旗舰店:650 万元
专卖店:580 万元
加盟店:500 万元

===== 各季度总销售额 =====
Q1:300 万元
Q2:390 万元
Q3:480 万元
Q4:560 万元

全年总销售额:1730 万元

最高单季销售额:旗舰店 Q4,200 万元

===== 各店铺平均季度销售额(LINQ) =====
旗舰店:162.5 万元
专卖店:145.0 万元
加盟店:125.0 万元

核心思路拆解:

  1. 按行求和:外层固定行,内层累加列 → 店铺年销售额
  2. 按列求和:外层固定列,内层累加行 → 季度总销售额
  3. Cast + Sum:LINQ 摊平后聚合 → 全年总销售额
  4. 双重循环找最大值:记录最大值的位置(行号+列号)
  5. Enumerable.Range + Select:LINQ 提取某一行数据,计算平均值

五、常见问题与避坑指南 ​

5.1 坑一:混淆 [,] 和 [][] 的声明与访问语法 ​

错误示例:

csharp
// 错误 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[][] 不能用逗号

修正方案:

csharp
// 记忆口诀:声明用什么,访问就用什么
multi[0, 0] = 1;        // int[,] → 逗号访问
jagged[0][0] = 1;       // int[][] → 双括号访问

5.2 坑二:GetLength 参数传反,导致越界 ​

错误示例:

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

修正方案:

csharp
// 记忆口诀: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 试图修改元素 ​

错误示例:

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

// 错误:foreach 的循环变量是只读副本
foreach (int score in scores)
{
    // score += 5;   // 编译错误!score 是只读的
}

修正方案:

csharp
// 用嵌套 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 直接对多维数组操作 ​

错误示例:

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

// 错误:多维数组不直接支持 LINQ
// var sum = scores.Sum();           // 编译错误!
// var max = scores.Max();           // 编译错误!

修正方案:

csharp
// 先用 Cast<T>() 摊平成一维序列
var allScores = scores.Cast<int>();
var sum = allScores.Sum();          // 正确
var max = allScores.Max();          // 正确

5.5 坑五:交错数组忘记 new 内层,或遍历时用固定长度 ​

错误示例:

csharp
// 错误 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] + " ");   // 越界异常!
    }
}

修正方案:

csharp
// 修正 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 ​

错误示例:

csharp
int[,] arr = new int[4, 3];

// 错误:以为 Length 是行数
// for (int i = 0; i < arr.Length; i++)   // i 会走到 11,越界!

// 错误:以为 Rank() 是方法
// Console.WriteLine(arr.Rank());          // 编译错误!Rank 是属性

修正方案:

csharp
// 正确理解三个概念
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 核心原则 ​

  1. 声明用什么,访问就用什么:[,] 用逗号,[][] 用双括号
  2. GetLength(0) 对应第一个索引,GetLength(1) 对应第二个索引
  3. foreach 不能改元素,要改就用 for 通过索引
  4. 多维数组用 LINQ 前先 Cast<T>() 摊平
  5. 交错数组内层必须先 new,遍历用 classes[i].Length 自适应

七、入门练习题 ​

  1. 遍历练习:定义一个 3×3 的二维数组 { {1,2,3}, {4,5,6}, {7,8,9} },分别用嵌套 for、foreach、LINQ 三种方式输出所有元素。

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

  3. 统计练习:有一个 4×3 的成绩数组,计算:

    • 每个学生的总分
    • 每门课的平均分
    • 全班最高分
  4. 交错数组练习:定义交错数组 { new[]{80,90}, new[]{70,85,95}, new[]{60} },找出所有分数中的最高分和最低分。

  5. 矩阵练习:实现两个 2×2 矩阵的减法,输出结果矩阵。

  6. 综合练习:有一个 2×2×2 的三维数组表示「2 个仓库 × 2 个货架 × 2 个货位」,统计:

    • 每个仓库的总库存
    • 库存最多的货位位置(仓库号、货架号、货位号)
  7. 思考题:下面这段代码输出什么?为什么?

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

csharp
int[,] b = (int[,])a.Clone();
b[0, 0] = 999;

掌握了多维数组的遍历和方法,你已经能处理大部分多维数据了。下一篇我们将学习交错数组的定义和基础使用,深入了解这种「数组的数组」与真多维数组的本质区别,以及它在实际开发中的独特价值。