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 列)为例,内存中的布局是:
逻辑结构(你看到的): 物理存储(内存中的样子):
┌─────┬─────┬─────┐
│ 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# 写法(本教程主线)
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:二维数组本质是「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:数组里装数组
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 原生:列表里装列表
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# | Java | JavaScript | Python |
|---|---|---|---|---|
| 声明二维 | 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.length | a.length | len(a) |
| 列数 | a.GetLength(1) | a[0].length | a[0].length | len(a[0]) |
⚠️ 注意 C# 特有的「逗号访问」:
a[1, 2]。其他语言都是a[1][2]。在 C# 里写a[1][2]访问int[,]会直接编译报错!
三、基础使用:访问、遍历、修改
3.1 元素访问与修改
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
}
}操作拆解:
scores[1, 2]:第一个索引是行(第 1 个学生),第二个索引是列(第 2 门课)。两个索引都从 0 开始。scores[3, 0] += 5:和一维数组一样,先读出旧值 65,加 5,再写回原位置。- 越界检查规则与一维数组相同:每一维的合法索引范围都是
0 ~ GetLength(维度) - 1,任何一维越界都会抛IndexOutOfRangeException。
关于「添加和删除」:和一维数组一样,多维数组长度创建即固定,无法直接添加或删除元素。所谓「增删」只能通过「新建一个更大/更小的数组,再把数据搬过去」实现(实战中一般用 List<List<T>> 替代,那是后续集合章节的内容)。
3.2 遍历方式一:嵌套 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=0 时,内层 j 从 0 走到 2,打印完整第 0 行;然后 i=1,内层再走一遍……可以记成「外层管换行,内层管走动」,就像老式打字机:打完一行字,按一下回车(换行),字车回到行首继续打。
3.3 遍历方式二: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优点:不需要关心维度、不会越界。缺点:拿不到行号列号、不能修改元素(副本机制,与一维数组相同)。
3.4 三维数组遍历
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 定义与创建
交错数组:「数组的数组」,每个内层数组长度可以不同。适合「每个班级人数不同」「每个月天数不同」这类参差不齐的数据。
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 遍历与注意事项
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();
}使用注意事项:
- 语法不同:声明
int[][],访问arr[行][列](两对括号),与int[,]的arr[行, 列]完全不同,混用直接编译报错 - 内层必须先 new:
new int[3][]之后每个内层都是null,直接访问arr[0][0]会抛NullReferenceException - 内层可为 null:遍历时若数据可能不完整,先判断
if (classes[i] != null) - 性能略逊于
[,]:内存不连续,多一次引用跳转;但换来的是灵活性
五、真实开发场景应用
5.1 场景一:图像像素处理
一张灰度图本质是「高度 × 宽度」的二维数组,每个元素是 0~255 的亮度值。
// 将一张 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 基础)
// 两个 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 场景三:多级分类数据存储
// 电商仓库: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 场景四:游戏地图开发
// 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 场景五:排课表(交错数组的主场)
// 一周课程表:每天课程节数不同,用交错数组最合适
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 误区一:维度越界——行数列数傻傻分不清
错误示例:
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,越界!
}
}修正方案:
// 记忆口诀: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:想复制一个二维数组,结果只复制了「外壳」
const original = [
[1, 2],
[3, 4],
];
const copy = [...original]; // 或 original.slice()——都是浅拷贝!
copy[0][0] = 999;
console.log(original[0][0]); // 999!原数组被污染了# Python:同样的坑
original = [[1, 2], [3, 4]]
copy = original[:] # 浅拷贝,内层列表还是共享的
copy[0][0] = 999
print(original[0][0]) # 999!原因:浅拷贝只复制了外层数组,内层数组的引用仍然共享——两个外壳里有相同的「纸条」,指向同一批内层数组。
修正方案:
// JavaScript:深拷贝二维数组
const copy = original.map((row) => [...row]); // 每一行都单独复制# Python:用 copy 模块的深拷贝
import copy as cp
new_copy = cp.deepcopy(original)// 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 误区三:交错数组用矩形思维遍历
错误示例:
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] + " ");
}
}修正方案:内层循环的上界永远用当前行自己的长度:
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# 中混用 [,] 和 [][] 的访问语法
错误示例:
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[][] 不能用逗号访问修正方案:记住对应关系——声明用什么,访问就用什么:
multi[0, 0] = 1; // int[,] → 逗号访问
jagged[0][0] = 1; // int[][] → 双括号访问七、知识点自检习题
概念题:
int[3, 4]数组的Rank、Length、GetLength(0)、GetLength(1)分别是多少?编程题:声明一个 3×3 的二维数组,填入 1~9,然后计算主对角线(左上到右下)元素之和。
编程题:有一个交错数组表示 3 个小组的分数:
{ {80, 90}, {70, 85, 95}, {60} },编写代码找出所有小组中的最高分。纠错题:以下代码有什么问题?如何修正?
int[][] data = new int[2][];
data[0][0] = 100;- 思考题:C# 中
int[,]用Clone()复制后修改副本不影响原数组,但int[][]却会,为什么?
答案解析
第 1 题:Rank = 2(二维),Length = 12(3×4 总元素数),GetLength(0) = 3(行数),GetLength(1) = 4(列数)。
第 2 题:
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 题:
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。
八、拓展学习指引
掌握了多维数组的定义和基础使用后,推荐按以下路径继续深入:
- 下一篇:多维数组的遍历和方法(Basic24)——学习
Array类针对多维数组的进阶操作、性能优化技巧 - 进阶方向一:
List<List<T>>等泛型集合——解决多维数组「长度固定」的痛点,实际业务开发的主流选择 - 进阶方向二:
Span<T>、Memory<T>——.NET 高性能场景下的内存操作利器(游戏、图像处理必学) - 进阶方向三:NumPy(Python)/ Math.NET(C#)——如果做科学计算或机器学习,这些库把多维数组封装得更强大
九、知识点总结
- 多维数组 = 多个索引定位元素:二维是表格(行,列),三维是一摞表格(层,行,列)
- C# 有两种多维结构:
int[,]真多维(连续内存、必须矩形)和int[][]交错数组(数组的数组、行长度随意) - 核心 API:
Rank看维度、Length看总元素数、GetLength(n)看第 n 维长度 - 访问语法不通用:
[,]用逗号a[i, j],[][]用双括号a[i][j],混用编译报错 - 遍历套路:几维套几层 for,每层上界用对应维度的
GetLength;只读场景可用 foreach 自动摊平 - 三大坑:维度越界(GetLength 参数对应错)、浅拷贝(交错数组 Clone 后内层仍共享)、交错数组忘 new 内层(NullReferenceException)
- 语言差异:Java/JS/Python 的「多维数组」本质是嵌套数组(双括号访问),C# 的
[,]才是真多维(逗号访问)