Skip to content

C# 多维数组的定义和基础使用:从一排队柜到一整面柜墙 ​

引言:当一排柜子不够用时 ​

前面几篇我们一直在玩「一排储物柜」——一维数组。但现实中很多数据不是一排能装下的:

  • 班级成绩单:4 个学生 × 3 门科目,是一张有行有列的表格
  • 超市货架:5 排货架 × 每排 6 层,找商品要说「第几排第几层」
  • 电影院座位:8 排 × 每排 10 座,票上印着「5 排 7 座」
  • 游戏地图:横坐标 + 纵坐标,定位每一个格子

这些数据的共同点是:需要两个编号才能定位一个元素。如果再往上加——一学期 4 个班、每个班 4 个学生、每个学生 3 门成绩,就需要三个编号了。

这就是多维数组存在的意义:用多个索引,精准定位多维空间中的每一个数据点。


一、核心定义:什么是多维数组 ​

1.1 官方定义 ​

多维数组:需要两个或更多个索引才能定位元素的数组。索引的个数称为维度(也叫「秩」,Rank)。

  • 二维数组:数组名[行, 列]——像 Excel 表格
  • 三维数组:数组名[层, 行, 列]——像一摞叠起来的表格
  • N 维数组:依次类推(实际开发中超过三维的极少见)

1.2 内存存储逻辑:连续的一大块 ​

C#(以及 Java、C)中的多维数组是真正意义上的连续内存块。以 int[2, 3](2 行 3 列)为例,内存中的布局是:

text
逻辑结构(你看到的):            物理存储(内存中的样子):
┌─────┬─────┬─────┐
│ 0,0 │ 0,1 │ 0,2 │  第0行      [0,0][0,1][0,2][1,0][1,1][1,2]
├─────┼─────┼─────┤            ↑ 一整块连续内存,按行优先排列
│ 1,0 │ 1,1 │ 1,2 │  第1行      地址:1000 → 1024(6个int×4字节)
└─────┴─────┴─────┘

访问 arr[1, 2] 时,计算机用公式「起始地址 + (行号 × 列数 + 列号) × 元素大小」一步算出地址:1000 + (1×3+2)×4 = 1020。这就是多维数组访问速度快的原因——和一维数组一样,都是 O(1) 直达。

1.3 两种实现模式:连续内存块 vs 数组的数组 ​

不同语言对「多维数据」有两种不同的实现思路:

模式代表内存结构特点
连续内存块(真多维数组)C# 的 int[,]、C 的 int[2][3]、NumPy 的 ndarray一整块连续内存访问快、必须矩形(每行等长)
数组的数组(交错/锯齿数组)C# 的 int[][]、Java 的 int[][]、JavaScript/Python 的嵌套列表外层数组存引用,各行分散在内存各处每行长度可以不同、灵活性高

Python 和 JavaScript 没有真正的多维数组:它们的「二维数组」本质是「列表里装列表」(数组的数组)。而 C# 两种都支持:[,] 是真多维,[][] 是交错数组。这一点后文会反复用到。


二、多维数组的创建与初始化 ​

2.1 C# 写法(本教程主线) ​

csharp
using System;

class Program
{
    static void Main()
    {
        // ===== 二维数组的三种创建方式 =====

        // 方式 1:只指定维度(所有元素自动填默认值 0)
        int[,] grid = new int[2, 3];          // 2 行 3 列

        // 方式 2:声明同时赋值(嵌套大括号,推荐)
        int[,] scores = {
            { 90, 85, 78 },    // 第 0 个学生的语数英成绩
            { 88, 92, 60 },    // 第 1 个学生
            { 76, 81, 95 },    // 第 2 个学生
            { 65, 70, 72 }     // 第 3 个学生
        };

        // 方式 3:完整写法(效果同上,更啰嗦)
        int[,] table = new int[2, 2] { { 1, 2 }, { 3, 4 } };

        // ===== 三维数组 =====
        // 2 个班 × 每班 2 个学生 × 每人 3 门成绩
        int[,,] school = new int[2, 2, 3];
        int[,,] data = {
            { { 1, 2, 3 }, { 4, 5, 6 } },        // 第 0 个班
            { { 7, 8, 9 }, { 10, 11, 12 } }      // 第 1 个班
        };

        // ===== 查看数组的「身份信息」=====
        Console.WriteLine(scores.Rank);        // 输出:2(维度数)
        Console.WriteLine(scores.Length);      // 输出:12(元素总数 = 4×3)
        Console.WriteLine(scores.GetLength(0)); // 输出:4(第 0 维长度 = 行数)
        Console.WriteLine(scores.GetLength(1)); // 输出:3(第 1 维长度 = 列数)
    }
}

关键 API 辨析(新手极易混淆):

成员含义int[4,3] 的返回值
Rank维度数(几维)2
Length所有元素总数12
GetLength(0)第 0 维(行)的长度4
GetLength(1)第 1 维(列)的长度3

2.2 其他语言对照 ​

Java:和 C# 不同,Java 只有「数组的数组」,没有 [,] 语法:

java
// Java:二维数组本质是「int 数组的数组」
int[][] scores = {
    { 90, 85, 78 },
    { 88, 92, 60 }
};
System.out.println(scores.length);     // 2(行数)
System.out.println(scores[0].length);  // 3(第 0 行的列数)
// Java 允许每行长度不同:new int[2][] 后逐行 new,天生就是交错结构

JavaScript:同样是嵌套数组,没有维度概念:

javascript
// JavaScript:数组里装数组
const scores = [
  [90, 85, 78],
  [88, 92, 60],
];
console.log(scores.length); // 2(行数)
console.log(scores[0].length); // 3(第 0 行长度)
// 访问元素:scores[1][2] → 60(两对独立方括号)

Python:嵌套列表,实际开发中更多用 NumPy:

python
# Python 原生:列表里装列表
scores = [
    [90, 85, 78],
    [88, 92, 60]
]
print(len(scores))      # 2(行数)
print(len(scores[0]))   # 3(列数)
print(scores[1][2])     # 60

# NumPy(真多维数组,科学计算标配):
# import numpy as np
# arr = np.array(scores)   # arr.shape → (2, 3),连续内存

语法差异速查:

操作C#JavaJavaScriptPython
声明二维int[,] a = new int[2,3]int[][] a = new int[2][3]let a = [[],[]]a = [[0]*3 for _ in range(2)]
访问元素a[1, 2](逗号)a[1][2](双括号)a[1][2]a[1][2]
行数a.GetLength(0)a.lengtha.lengthlen(a)
列数a.GetLength(1)a[0].lengtha[0].lengthlen(a[0])

⚠️ 注意 C# 特有的「逗号访问」:a[1, 2]。其他语言都是 a[1][2]。在 C# 里写 a[1][2] 访问 int[,] 会直接编译报错!


三、基础使用:访问、遍历、修改 ​

3.1 元素访问与修改 ​

csharp
using System;

class Program
{
    static void Main()
    {
        // 4 个学生 × 3 门课的成绩表
        int[,] scores = {
            { 90, 85, 78 },
            { 88, 92, 60 },
            { 76, 81, 95 },
            { 65, 70, 72 }
        };

        // 【读】取出第 1 个学生的第 2 门成绩(索引从 0 开始!)
        Console.WriteLine(scores[1, 2]);     // 输出:60

        // 【改】给第 3 个学生的第 1 门成绩加 5 分
        scores[3, 0] += 5;                    // 65 → 70
        Console.WriteLine(scores[3, 0]);     // 输出:70

        // 【边界】最后一个元素的索引是「每维长度 - 1」
        int lastRow = scores.GetLength(0) - 1;    // 3
        int lastCol = scores.GetLength(1) - 1;    // 2
        Console.WriteLine(scores[lastRow, lastCol]);  // 输出:72
    }
}

操作拆解:

  1. scores[1, 2]:第一个索引是行(第 1 个学生),第二个索引是列(第 2 门课)。两个索引都从 0 开始。
  2. scores[3, 0] += 5:和一维数组一样,先读出旧值 65,加 5,再写回原位置。
  3. 越界检查规则与一维数组相同:每一维的合法索引范围都是 0 ~ GetLength(维度) - 1,任何一维越界都会抛 IndexOutOfRangeException。

关于「添加和删除」:和一维数组一样,多维数组长度创建即固定,无法直接添加或删除元素。所谓「增删」只能通过「新建一个更大/更小的数组,再把数据搬过去」实现(实战中一般用 List<List<T>> 替代,那是后续集合章节的内容)。

3.2 遍历方式一:嵌套 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

执行顺序拆解:外层 i=0 时,内层 j 从 0 走到 2,打印完整第 0 行;然后 i=1,内层再走一遍……可以记成「外层管换行,内层管走动」,就像老式打字机:打完一行字,按一下回车(换行),字车回到行首继续打。

3.3 遍历方式二: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

优点:不需要关心维度、不会越界。缺点:拿不到行号列号、不能修改元素(副本机制,与一维数组相同)。

3.4 三维数组遍历 ​

csharp
int[,,] school = {
    { { 1, 2 }, { 3, 4 } },      // 第 0 个班:2 组 × 2 人
    { { 5, 6 }, { 7, 8 } }       // 第 1 个班
};

// 三重循环:班 → 组 → 人
for (int c = 0; c < school.GetLength(0); c++)
{
    for (int g = 0; g < school.GetLength(1); g++)
    {
        for (int s = 0; s < school.GetLength(2); s++)
        {
            Console.WriteLine($"班{c} 组{g} 人{s}:{school[c, g, s]}");
        }
    }
}

规律:几维数组就套几层循环,每层循环的上界用 GetLength(对应维度)。


四、不规则数组(交错数组 Jagged Array)专题 ​

4.1 定义与创建 ​

交错数组:「数组的数组」,每个内层数组长度可以不同。适合「每个班级人数不同」「每个月天数不同」这类参差不齐的数据。

csharp
using System;

class Program
{
    static void Main()
    {
        // 声明:两对独立方括号
        int[][] classes = new int[3][];   // 3 个班,内层目前全是 null!

        // 必须逐班 new(这是和一维引用类型数组一样的坑)
        classes[0] = new int[] { 90, 85 };          // 0 班 2 人
        classes[1] = new int[] { 88, 92, 60, 71 };  // 1 班 4 人
        classes[2] = new int[] { 76 };              // 2 班 1 人

        // 也可以声明时一次性初始化
        int[][] groups = {
            new[] { 1, 2, 3 },
            new[] { 4, 5 },
            new[] { 6, 7, 8, 9 }
        };

        Console.WriteLine(classes[1][2]);    // 输出:60(注意是两对括号!)
    }
}

4.2 遍历与注意事项 ​

csharp
int[][] classes = {
    new[] { 90, 85 },
    new[] { 88, 92, 60, 71 },
    new[] { 76 }
};

// 外层 for 走班级,内层 foreach 走学生(内层长度各不相同,用 .Length 自适应)
for (int i = 0; i < classes.Length; i++)
{
    Console.Write($"第 {i} 班({classes[i].Length} 人):");
    foreach (int score in classes[i])
    {
        Console.Write(score + " ");
    }
    Console.WriteLine();
}

使用注意事项:

  1. 语法不同:声明 int[][],访问 arr[行][列](两对括号),与 int[,] 的 arr[行, 列] 完全不同,混用直接编译报错
  2. 内层必须先 new:new int[3][] 之后每个内层都是 null,直接访问 arr[0][0] 会抛 NullReferenceException
  3. 内层可为 null:遍历时若数据可能不完整,先判断 if (classes[i] != null)
  4. 性能略逊于 [,]:内存不连续,多一次引用跳转;但换来的是灵活性

五、真实开发场景应用 ​

5.1 场景一:图像像素处理 ​

一张灰度图本质是「高度 × 宽度」的二维数组,每个元素是 0~255 的亮度值。

csharp
// 将一张 4×4 灰度图「反色」(255 - 原值)
int[,] image = {
    { 0, 50, 100, 150 },
    { 30, 80, 130, 180 },
    { 60, 110, 160, 210 },
    { 90, 140, 190, 255 }
};

for (int y = 0; y < image.GetLength(0); y++)       // y = 行(纵坐标)
{
    for (int x = 0; x < image.GetLength(1); x++)   // x = 列(横坐标)
    {
        image[y, x] = 255 - image[y, x];   // 反色公式
    }
}
// 适配逻辑:图像处理天然是逐像素操作,二维索引 (y, x) 与屏幕坐标一一对应

5.2 场景二:矩阵运算(科学计算、AI 基础) ​

csharp
// 两个 2×2 矩阵相加:对应位置元素相加
int[,] a = { { 1, 2 }, { 3, 4 } };
int[,] b = { { 5, 6 }, { 7, 8 } };
int[,] result = new int[2, 2];

for (int i = 0; i < 2; i++)
{
    for (int j = 0; j < 2; j++)
    {
        result[i, j] = a[i, j] + b[i, j];   // 结果为 { {6, 8}, {10, 12} }
    }
}
// 适配逻辑:矩阵的行列结构和二维数组完美对应,机器学习中的张量就是 N 维数组

5.3 场景三:多级分类数据存储 ​

csharp
// 电商仓库:2 个仓库 × 每个仓库 3 个货架 × 每架 4 个货位,存库存数量
int[,,] warehouse = new int[2, 3, 4];

warehouse[0, 1, 2] = 500;   // 0 号仓库 1 号货架 2 号货位,补货 500 件
Console.WriteLine(warehouse[0, 1, 2]);   // 输出:500
// 适配逻辑:层级递进的位置关系(仓库→货架→货位)用三维索引表达最直观

5.4 场景四:游戏地图开发 ​

csharp
// 5×5 游戏地图:0=草地 1=墙 2=河流 3=宝物
int[,] map = {
    { 1, 1, 1, 1, 1 },
    { 1, 0, 0, 2, 1 },
    { 1, 0, 3, 0, 1 },
    { 1, 2, 0, 0, 1 },
    { 1, 1, 1, 1, 1 }
};

// 玩家从 (1,1) 向右移动一格,检查能否通行
int playerX = 1, playerY = 1;
int nextX = playerX + 1;

if (map[playerY, nextX] == 0)          // 目标是草地才能走
{
    playerX = nextX;
    Console.WriteLine($"移动成功,当前位置:({playerY}, {playerX})");
}
else
{
    Console.WriteLine("撞墙了,无法移动!");
}
// 适配逻辑:地图格子与二维索引天然对应,碰撞检测就是查一下目标格子的值

5.5 场景五:排课表(交错数组的主场) ​

csharp
// 一周课程表:每天课程节数不同,用交错数组最合适
string[][] weekSchedule = {
    new[] { "数学", "英语", "物理" },          // 周一 3 节
    new[] { "语文", "化学" },                  // 周二 2 节
    new[] { "数学", "体育", "音乐", "美术" },   // 周三 4 节
    new string[0],                              // 周四没课(空数组,不是 null!)
    new[] { "英语" }                            // 周五 1 节
};

string[] dayNames = { "周一", "周二", "周三", "周四", "周五" };
for (int i = 0; i < weekSchedule.Length; i++)
{
    Console.WriteLine($"{dayNames[i]}:{weekSchedule[i].Length} 节课");
}
// 适配逻辑:每天节数不同(参差不齐),用二维数组会浪费空间,交错数组按需分配

六、常见误区解析 ​

6.1 误区一:维度越界——行数列数傻傻分不清 ​

错误示例:

csharp
int[,] grid = new int[2, 3];   // 2 行 3 列
// grid[2, 0] = 1;             // 运行时异常!行索引最大是 1
// grid[0, 3] = 1;             // 运行时异常!列索引最大是 2

// 更隐蔽的坑:GetLength 参数传反
for (int i = 0; i < grid.GetLength(1); i++)        // 错!用列数控制行循环
{
    for (int j = 0; j < grid.GetLength(0); j++)    // 错!用行数控制列循环
    {
        Console.WriteLine(grid[i, j]);             // i 会走到 2,越界!
    }
}

修正方案:

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

6.2 误区二:浅拷贝——改了副本,原件跟着变 ​

错误示例(JavaScript/Python 中最常见):

javascript
// JavaScript:想复制一个二维数组,结果只复制了「外壳」
const original = [
  [1, 2],
  [3, 4],
];
const copy = [...original]; // 或 original.slice()——都是浅拷贝!

copy[0][0] = 999;
console.log(original[0][0]); // 999!原数组被污染了
python
# Python:同样的坑
original = [[1, 2], [3, 4]]
copy = original[:]          # 浅拷贝,内层列表还是共享的
copy[0][0] = 999
print(original[0][0])       # 999!

原因:浅拷贝只复制了外层数组,内层数组的引用仍然共享——两个外壳里有相同的「纸条」,指向同一批内层数组。

修正方案:

javascript
// JavaScript:深拷贝二维数组
const copy = original.map((row) => [...row]); // 每一行都单独复制
python
# Python:用 copy 模块的深拷贝
import copy as cp
new_copy = cp.deepcopy(original)
csharp
// C#:真多维数组 int[,] 用 Clone 就是完整复制(整块内存拷贝),没有这个问题
int[,] a = { { 1, 2 }, { 3, 4 } };
int[,] b = (int[,])a.Clone();
b[0, 0] = 999;
Console.WriteLine(a[0, 0]);   // 1(不受影响)

// 但交错数组 int[][] 的 Clone 也是浅拷贝!内层共享
int[][] j1 = { new[] { 1, 2 }, new[] { 3, 4 } };
int[][] j2 = (int[][])j1.Clone();
j2[0][0] = 999;
Console.WriteLine(j1[0][0]);  // 999(被污染!需要逐行 Clone)

6.3 误区三:交错数组用矩形思维遍历 ​

错误示例:

csharp
int[][] jagged = {
    new[] { 1, 2 },
    new[] { 3, 4, 5, 6 },
    new[] { 7 }
};

// 错误:假设每行都是 2 列
for (int i = 0; i < jagged.Length; i++)
{
    for (int j = 0; j < 2; j++)     // 第 1 行只有 2 个元素没问题,但...
    {
        // 如果写成 j < 4,遍历到第 0 行和第 2 行时就会越界!
        Console.Write(jagged[i][j] + " ");
    }
}

修正方案:内层循环的上界永远用当前行自己的长度:

csharp
for (int i = 0; i < jagged.Length; i++)
{
    for (int j = 0; j < jagged[i].Length; j++)   // 关键:jagged[i].Length
    {
        Console.Write(jagged[i][j] + " ");
    }
    Console.WriteLine();
}

6.4 误区四:C# 中混用 [,] 和 [][] 的访问语法 ​

错误示例:

csharp
int[,] multi = new int[2, 3];
// multi[0][0] = 1;      // 编译错误!int[,] 不能用两对括号访问

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[][] → 双括号访问

七、知识点自检习题 ​

  1. 概念题:int[3, 4] 数组的 Rank、Length、GetLength(0)、GetLength(1) 分别是多少?

  2. 编程题:声明一个 3×3 的二维数组,填入 1~9,然后计算主对角线(左上到右下)元素之和。

  3. 编程题:有一个交错数组表示 3 个小组的分数:{ {80, 90}, {70, 85, 95}, {60} },编写代码找出所有小组中的最高分。

  4. 纠错题:以下代码有什么问题?如何修正?

csharp
int[][] data = new int[2][];
data[0][0] = 100;
  1. 思考题:C# 中 int[,] 用 Clone() 复制后修改副本不影响原数组,但 int[][] 却会,为什么?

答案解析 ​

第 1 题:Rank = 2(二维),Length = 12(3×4 总元素数),GetLength(0) = 3(行数),GetLength(1) = 4(列数)。

第 2 题:

csharp
int[,] matrix = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
int sum = 0;
for (int i = 0; i < 3; i++)
{
    sum += matrix[i, i];   // 主对角线的特征:行号 == 列号
}
Console.WriteLine(sum);    // 输出:15(1+5+9)

第 3 题:

csharp
int[][] groups = { new[] { 80, 90 }, new[] { 70, 85, 95 }, new[] { 60 } };
int max = groups[0][0];          // 先假设第一个元素最大
foreach (int[] group in groups)
{
    foreach (int score in group)
    {
        if (score > max) max = score;
    }
}
Console.WriteLine(max);          // 输出:95

第 4 题:new int[2][] 只创建了外层数组,data[0] 是 null,直接访问 data[0][0] 会抛 NullReferenceException。修正:先 data[0] = new int[1]; 再赋值。

第 5 题:int[,] 是一整块连续内存,Clone() 把整块内存完整复制一份,新旧数组完全独立。而 int[][] 是「数组的数组」,Clone() 只复制外层数组,外层数组里装的内层引用被原样复制——新旧外层数组指向同一批内层数组,改内层元素自然互相影响。要深拷贝交错数组,需要逐行 Clone。


八、拓展学习指引 ​

掌握了多维数组的定义和基础使用后,推荐按以下路径继续深入:

  1. 下一篇:多维数组的遍历和方法(Basic24)——学习 Array 类针对多维数组的进阶操作、性能优化技巧
  2. 进阶方向一:List<List<T>> 等泛型集合——解决多维数组「长度固定」的痛点,实际业务开发的主流选择
  3. 进阶方向二:Span<T>、Memory<T>——.NET 高性能场景下的内存操作利器(游戏、图像处理必学)
  4. 进阶方向三:NumPy(Python)/ Math.NET(C#)——如果做科学计算或机器学习,这些库把多维数组封装得更强大

九、知识点总结 ​

  1. 多维数组 = 多个索引定位元素:二维是表格(行,列),三维是一摞表格(层,行,列)
  2. C# 有两种多维结构:int[,] 真多维(连续内存、必须矩形)和 int[][] 交错数组(数组的数组、行长度随意)
  3. 核心 API:Rank 看维度、Length 看总元素数、GetLength(n) 看第 n 维长度
  4. 访问语法不通用:[,] 用逗号 a[i, j],[][] 用双括号 a[i][j],混用编译报错
  5. 遍历套路:几维套几层 for,每层上界用对应维度的 GetLength;只读场景可用 foreach 自动摊平
  6. 三大坑:维度越界(GetLength 参数对应错)、浅拷贝(交错数组 Clone 后内层仍共享)、交错数组忘 new 内层(NullReferenceException)
  7. 语言差异:Java/JS/Python 的「多维数组」本质是嵌套数组(双括号访问),C# 的 [,] 才是真多维(逗号访问)