CSharp 交错数组的定义和基础使用:你的「不规则货架」
引言:当 Excel 表格装不下你的数据
上一篇我们学会了多维数组——一张规规矩矩的 Excel 表格,每行每列都整整齐齐。但现实中很多数据是参差不齐的:
- 班级成绩单:1 班 30 人,2 班 25 人,3 班 32 人——每行人数不同
- 每月销售额:1 月 31 天,2 月 28 天,4 月 30 天——每行天数不同
- 电商订单:用户 A 买了 3 件商品,用户 B 买了 1 件,用户 C 买了 5 件——每单商品数不同
用二维数组装这些数据,就像把不同长度的货物硬塞进统一规格的货架——短的地方空着浪费,长的地方塞不下。
这就是交错数组存在的意义:每行长度可以不同,按需分配,不浪费空间。
一、交错数组是什么?和普通多维数组有什么区别?
1.1 一句话定义
交错数组(Jagged Array):数组的数组。外层是一个数组,里面的每个元素又是一个独立的一维数组,每个内层数组的长度可以不同。
用生活类比:
| 类比 | 普通多维数组 int[,] | 交错数组 int[][] |
|---|---|---|
| 货架 | 一整面统一规格的货架墙 | 一排独立货架,每个货架层数不同 |
| Excel | 标准表格,每行列数相同 | 多行数据,每行列数随意 |
| 快递柜 | 统一规格的格子柜 | 大中小三种柜子组合 |
1.2 内存结构对比(核心差异)
普通多维数组 int[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字节)
└─────┴─────┴─────┘交错数组 int[][]——外层存引用,内层分散各处:
逻辑结构(你看到的): 物理存储(内存中的样子):
┌─────────┐ 外层数组(连续):
│ 第0行 → ├──┐ [引用0][引用1][引用2]
├─────────┤ │ ↓ ↓ ↓
│ 第1行 → ├──┼──┐ ┌───┐ ┌───┐ ┌───┐
├─────────┤ │ │ │1,2│ │3,4│ │5 │
│ 第2行 → ├──┘ │ └───┘ └───┘ └───┘
└─────────┘ │ 地址2000 地址3000 地址4000
│ ↑ 内层数组分散在内存各处
└──┐
┌───┐
│5 │
└───┘关键区别总结:
| 对比项 | 多维数组 int[,] | 交错数组 int[][] |
|---|---|---|
| 内存结构 | 一整块连续内存 | 外层连续,内层分散 |
| 每行长度 | 必须相同(矩形) | 可以不同(锯齿) |
| 访问语法 | arr[i, j](逗号) | arr[i][j](双括号) |
| 声明语法 | new int[2, 3] | new int[2][] 后逐行 new |
| 访问速度 | 更快(O(1) 直达) | 略慢(多一次引用跳转) |
| 适用场景 | 规则表格数据 | 不规则分组数据 |
1.3 为什么叫「交错」或「锯齿」
因为每行长度不同,画出来像锯齿一样参差不齐:
int[][] jagged = {
new[] { 1, 2 }, // 第0行:2个元素 ██
new[] { 3, 4, 5, 6 }, // 第1行:4个元素 ████
new[] { 7 } // 第2行:1个元素 █
};
// 画出来就是: ██
// ████
// █
// 像不像锯齿?所以叫 Jagged(锯齿状)二、交错数组的声明与初始化全场景详解
2.1 语法符号重点标注
交错数组的语法有两个核心符号规则,新手极易混淆:
| 符号 | 位置 | 含义 | 记忆口诀 |
|---|---|---|---|
[][] | 类型声明处 | 「数组的数组」,两对独立方括号 | 两对括号 = 交错 |
, | 多维数组中 | 一个方括号里的逗号,分隔维度 | 一个逗号 = 多维 |
[] | 访问时 | 每对括号对应一层数组 | 几层数组几对括号 |
对比记忆:
int[,] multi; // 多维数组:一个方括号 + 逗号 → 访问用 arr[i, j]
int[][] jagged; // 交错数组:两个独立方括号 → 访问用 arr[i][j]2.2 声明方式一:只声明外层(最常用)
语法:数据类型[][] 数组名 = new 数据类型[外层长度][];
using System;
class Program
{
static void Main()
{
// 声明一个交错数组:外层有 3 个元素,每个元素是 int[]
// 注意:此时内层数组全是 null!还没创建!
int[][] classes = new int[3][];
// 必须逐行 new,给每个内层数组分配空间
classes[0] = new int[2]; // 第0行:2个元素,默认都是 0
classes[1] = new int[4]; // 第1行:4个元素,默认都是 0
classes[2] = new int[1]; // 第2行:1个元素,默认是 0
// 现在可以安全访问了
classes[0][0] = 90; // 第0行第0列 = 90
classes[1][2] = 85; // 第1行第2列 = 85
Console.WriteLine(classes[0][0]); // 输出:90
Console.WriteLine(classes[1][2]); // 输出:85
}
}逐行拆解:
int[][] classes = new int[3][];—— 声明外层数组,长度 3,内层全是 nullclasses[0] = new int[2];—— 给第 0 个内层数组分配空间,长度 2classes[1] = new int[4];—— 第 1 个内层数组,长度 4(和第 0 个不同!)classes[0][0] = 90;—— 访问:第一对括号[0]定位到内层数组,第二对括号[0]定位到元素
⚠️ 关键提醒:new int[3][] 只创建了外层,内层全是 null。如果不 new 内层就直接访问,会抛 NullReferenceException!
2.3 声明方式二:声明时直接初始化(推荐)
语法:数据类型[][] 数组名 = { new 数据类型[]{...}, new 数据类型[]{...} };
using System;
class Program
{
static void Main()
{
// 声明时直接初始化:每个内层数组用 new int[]{...} 创建
int[][] scores = {
new int[] { 90, 85 }, // 第0行:2个元素
new int[] { 88, 92, 60, 71 }, // 第1行:4个元素
new int[] { 76 } // 第2行:1个元素
};
// 可以省略内层的 new int[],让编译器推断(C# 3.0+)
int[][] grades = {
new[] { 90, 85 }, // 等价于 new int[] { 90, 85 }
new[] { 88, 92, 60, 71 },
new[] { 76 }
};
// 访问元素
Console.WriteLine(scores[1][2]); // 输出:60(第1行第2列)
Console.WriteLine(grades[2][0]); // 输出:76(第2行第0列)
}
}逐行拆解:
new int[] { 90, 85 }—— 创建第 0 个内层数组,直接放入 2 个元素new int[] { 88, 92, 60, 71 }—— 第 1 个内层数组,4 个元素(长度不同!)new[] { 76 }—— 省略int,编译器根据内容自动推断类型scores[1][2]—— 第一对[1]拿到第 1 个内层数组{88, 92, 60, 71},第二对[2]拿到第 2 个元素60
2.4 声明方式三:先声明后赋值(分步进行)
// 第一步:声明变量(此时是 null)
int[][] data;
// 第二步:创建外层数组
data = new int[2][];
// 第三步:创建内层数组
data[0] = new int[] { 1, 2, 3 };
data[1] = new int[] { 4, 5 };2.5 三维交错数组
// 三维交错:数组的数组的数组
int[][][] cube = new int[2][][];
// 第一层:2 个二维交错数组
cube[0] = new int[2][];
cube[0][0] = new int[] { 1, 2 };
cube[0][1] = new int[] { 3, 4, 5 };
cube[1] = new int[1][];
cube[1][0] = new int[] { 6 };
// 访问:三对括号
Console.WriteLine(cube[0][1][2]); // 输出:5规律:几维交错数组就有几对 [],访问时就写几对 []。
三、核心基础操作的完整实现示例
3.1 元素访问与修改
using System;
class Program
{
static void Main()
{
// 3 个班级,人数不同
int[][] classes = {
new int[] { 90, 85 }, // 0班:2人
new int[] { 88, 92, 60, 71 }, // 1班:4人
new int[] { 76 } // 2班:1人
};
// 【读】取出 1 班的第 2 个成绩
Console.WriteLine(classes[1][2]); // 输出:60
// 【改】给 0 班的第 0 个成绩加 5 分
classes[0][0] += 5; // 90 → 95
Console.WriteLine(classes[0][0]); // 输出:95
// 【边界】每行的最后一个元素索引 = 该行长度 - 1
int lastOfRow0 = classes[0][classes[0].Length - 1];
Console.WriteLine(lastOfRow0); // 输出:85
// 【越界】任何一维越界都会抛异常
// Console.WriteLine(classes[1][4]); // IndexOutOfRangeException!1班只有4人,索引最大是3
}
}操作拆解:
classes[1][2]—— 第一对括号[1]拿到new int[] { 88, 92, 60, 71 },第二对括号[2]拿到60classes[0][0] += 5—— 先读出旧值 90,加 5,再写回原位置classes[0].Length—— 获取第 0 行的长度(2),classes[0].Length - 1是该行最后一个元素的索引
3.2 数组长度获取
int[][] classes = {
new int[] { 90, 85 }, // 长度 2
new int[] { 88, 92, 60, 71 }, // 长度 4
new int[] { 76 } // 长度 1
};
// 外层长度:有几个内层数组
Console.WriteLine(classes.Length); // 输出:3
// 内层长度:每个内层数组各自的长度
Console.WriteLine(classes[0].Length); // 输出:2
Console.WriteLine(classes[1].Length); // 输出:4
Console.WriteLine(classes[2].Length); // 输出:1
// 总元素数:需要手动累加
int total = 0;
for (int i = 0; i < classes.Length; i++)
{
total += classes[i].Length;
}
Console.WriteLine(total); // 输出:7(2+4+1)关键区别:
| 属性 | 多维数组 int[,] | 交错数组 int[][] |
|---|---|---|
| 行数 | GetLength(0) | Length |
| 第 i 行列数 | GetLength(1) | [i].Length |
| 总元素数 | Length | 需手动累加 |
3.3 遍历方式一:嵌套 for 循环(全能选手)
int[][] classes = {
new int[] { 90, 85 },
new int[] { 88, 92, 60, 71 },
new int[] { 76 }
};
// 外层 for:走班级(需要班号)
for (int i = 0; i < classes.Length; i++)
{
Console.Write($"班级 {i}({classes[i].Length} 人):");
// 内层 for:走学生(需要学号),上界用「当前行自己的长度」
for (int j = 0; j < classes[i].Length; j++)
{
Console.Write($"{classes[i][j]} ");
}
Console.WriteLine();
}输出:
班级 0(2 人):90 85
班级 1(4 人):88 92 60 71
班级 2(1 人):76关键:内层循环上界是 classes[i].Length(当前行的长度),不是固定值!
3.4 遍历方式二:foreach 循环(最省心)
int[][] classes = {
new int[] { 90, 85 },
new int[] { 88, 92, 60, 71 },
new int[] { 76 }
};
// 外层 foreach:拿到的是「每一行」(int[] 类型)
foreach (int[] classScores in classes)
{
// 内层 foreach:遍历当前行的每个元素
foreach (int score in classScores)
{
Console.Write(score + " ");
}
Console.WriteLine();
}优点:不需要关心长度、不会越界。 缺点:拿不到班号和学号、不能修改元素。
3.5 遍历方式三:for + foreach 混合(推荐)
// 外层 for:需要班号来显示
for (int i = 0; i < classes.Length; i++)
{
Console.Write($"班级 {i}:");
// 内层 foreach:只读遍历当前班,简洁安全
foreach (int score in classes[i])
{
Console.Write(score + " ");
}
Console.WriteLine();
}3.6 遍历方式四:LINQ 查询
using System.Linq;
int[][] classes = {
new int[] { 90, 85 },
new int[] { 88, 92, 60, 71 },
new int[] { 76 }
};
// SelectMany:把所有班级摊平成一个序列
var allScores = classes.SelectMany(c => c);
Console.WriteLine($"全校总分:{allScores.Sum()}");
Console.WriteLine($"全校平均分:{allScores.Average():F1}");
// Select:对每个班级做聚合
var classAverages = classes.Select(c => c.Average());
foreach (var avg in classAverages)
{
Console.WriteLine($"班级平均分:{avg:F1}");
}
// Where + SelectMany:筛选出所有及格的成绩
var passed = classes.SelectMany(c => c).Where(s => s >= 60);
Console.WriteLine($"及格人数:{passed.Count()}");四、常见踩坑点与最佳实践
4.1 坑一:忘记 new 内层,直接访问
错误代码:
int[][] jagged = new int[3][];
// jagged[0][0] = 1; // 运行时异常!jagged[0] 是 null错误信息:NullReferenceException: Object reference not set to an instance of an object.
原因:new int[3][] 只创建了外层数组,3 个内层槽位全是 null,就像买了 3 个空货架,还没放隔板就想放东西。
修正:
int[][] jagged = new int[3][];
jagged[0] = new int[2]; // 先给第 0 个货架装隔板
jagged[0][0] = 1; // 现在可以放东西了4.2 坑二:遍历时用固定长度,导致越界
错误代码:
int[][] classes = {
new int[] { 90, 85 }, // 2 人
new int[] { 88, 92, 60, 71 }, // 4 人
new int[] { 76 } // 1 人
};
for (int i = 0; i < classes.Length; i++)
{
for (int j = 0; j < 4; j++) // 错误!假设每行都是 4 人
{
Console.Write(classes[i][j] + " "); // 第 0 行和第 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();
}4.3 坑三:混淆 [,] 和 [][] 的语法
错误代码:
int[][] jagged = new int[2][];
jagged[0] = new int[3];
// jagged[0, 0] = 1; // 编译错误!交错数组不能用逗号访问
int[,] multi = new int[2, 3];
// multi[0][0] = 1; // 编译错误!多维数组不能用双括号访问修正:记住对应关系——声明用什么,访问就用什么:
jagged[0][0] = 1; // int[][] → 双括号
multi[0, 0] = 1; // int[,] → 逗号4.4 坑四:浅拷贝——Clone 后内层仍共享
错误代码:
int[][] original = {
new int[] { 1, 2 },
new int[] { 3, 4 }
};
int[][] copy = (int[][])original.Clone(); // 浅拷贝!
copy[0][0] = 999;
Console.WriteLine(original[0][0]); // 输出:999(被污染!)原因:Clone() 只复制了外层数组,内层数组的引用被原样复制——两个外层数组指向同一批内层数组。
修正:逐行深拷贝:
int[][] copy = new int[original.Length][];
for (int i = 0; i < original.Length; i++)
{
copy[i] = (int[])original[i].Clone(); // 每行单独复制
}4.5 坑五:内层数组为 null 时直接遍历
错误代码:
int[][] data = new int[3][];
data[0] = new int[] { 1, 2 };
// data[1] 忘记 new 了,是 null
data[2] = new int[] { 3 };
foreach (int[] row in data)
{
foreach (int n in row) // 轮到 data[1] 时抛 NullReferenceException!
{
Console.Write(n + " ");
}
}修正:遍历时先判空:
foreach (int[] row in data)
{
if (row != null) // 防御性判断
{
foreach (int n in row)
{
Console.Write(n + " ");
}
}
}4.6 最佳实践清单
| 场景 | 推荐做法 |
|---|---|
| 声明交错数组 | 优先用 new[] { ... } 直接初始化,避免 null |
| 遍历 | 外层 for + 内层 foreach,兼顾索引和简洁 |
| 修改元素 | 必须用嵌套 for 通过索引修改 |
| 复制数组 | 逐行 Clone(),不要直接 Clone() 外层 |
| 判空 | 访问内层前判断 if (arr[i] != null) |
| 选择类型 | 规则数据用 [,],不规则数据用 [][] |
五、综合实战案例:学生成绩统计系统
需求:统计 3 个班级的成绩,每个班级人数不同。计算:
- 每个班级的平均分、最高分、最低分
- 全校平均分
- 找出全校最高分的学生(班级号 + 学号)
- 统计每个班级的及格率(≥60 分)
using System;
using System.Linq;
class Program
{
static void Main()
{
// 3 个班级,人数不同:2人、4人、3人
int[][] classes = {
new int[] { 90, 85 }, // 0 班
new int[] { 88, 92, 60, 71 }, // 1 班
new int[] { 76, 55, 82 } // 2 班
};
// ===== 1. 每个班级的统计 =====
Console.WriteLine("===== 各班级成绩统计 =====");
for (int i = 0; i < classes.Length; i++)
{
int sum = 0;
int max = classes[i][0];
int min = classes[i][0];
int passCount = 0;
for (int j = 0; j < classes[i].Length; j++)
{
int score = classes[i][j];
sum += score;
if (score > max) max = score;
if (score < min) min = score;
if (score >= 60) passCount++;
}
double avg = (double)sum / classes[i].Length;
double passRate = (double)passCount / classes[i].Length * 100;
Console.WriteLine($"班级 {i}:{classes[i].Length} 人");
Console.WriteLine($" 平均分:{avg:F1},最高分:{max},最低分:{min}");
Console.WriteLine($" 及格率:{passRate:F1}%");
}
// ===== 2. 全校统计(用 LINQ 摊平) =====
Console.WriteLine("\n===== 全校统计 =====");
var allScores = classes.SelectMany(c => c);
Console.WriteLine($"全校总人数:{allScores.Count()}");
Console.WriteLine($"全校平均分:{allScores.Average():F1}");
Console.WriteLine($"全校最高分:{allScores.Max()}");
Console.WriteLine($"全校最低分:{allScores.Min()}");
// ===== 3. 找出全校最高分的位置 =====
int globalMax = 0;
int maxClass = 0, maxStudent = 0;
for (int i = 0; i < classes.Length; i++)
{
for (int j = 0; j < classes[i].Length; j++)
{
if (classes[i][j] > globalMax)
{
globalMax = classes[i][j];
maxClass = i;
maxStudent = j;
}
}
}
Console.WriteLine($"\n全校最高分:班级 {maxClass},学号 {maxStudent},分数 {globalMax}");
// ===== 4. 给全班加 5 分(修改元素,必须用嵌套 for) =====
Console.WriteLine("\n===== 全班加 5 分后 =====");
for (int i = 0; i < classes.Length; i++)
{
for (int j = 0; j < classes[i].Length; j++)
{
classes[i][j] += 5; // 通过索引直接修改
}
}
// 重新统计加分后的全校平均分
var boostedScores = classes.SelectMany(c => c);
Console.WriteLine($"加分后全校平均分:{boostedScores.Average():F1}");
}
}输出:
===== 各班级成绩统计 =====
班级 0:2 人
平均分:87.5,最高分:90,最低分:85
及格率:100.0%
班级 1:4 人
平均分:77.8,最高分:92,最低分:60
及格率:100.0%
班级 2:3 人
平均分:71.0,最高分:82,最低分:55
及格率:66.7%
===== 全校统计 =====
全校总人数:9
全校平均分:77.6
全校最高分:92
全校最低分:55
全校最高分:班级 1,学号 1,分数 92
===== 全班加 5 分后 =====
加分后全校平均分:82.6核心思路拆解:
- 交错数组存储:每个班级人数不同,用
int[][]按需分配 - 嵌套 for 统计:外层走班级,内层走学生,逐班计算平均分、最高分、最低分、及格率
- SelectMany 摊平:把 3 个班级合成一个序列,用 LINQ 快速算全校统计
- 双重循环找最值:记录最高分的班级号和学号
- 索引修改:
classes[i][j] += 5直接修改原数组
六、知识点总结
- 交错数组 = 数组的数组:外层存引用,内层分散,每行长度可以不同
- 语法规则:声明用
int[][],访问用arr[i][j],与int[,]的arr[i, j]完全不同 - 初始化:
new int[3][]后必须逐行new,否则内层是null - 遍历:外层
classes.Length,内层classes[i].Length,几层数组几对括号 - 四大坑:忘 new 内层、固定长度遍历、混用
[,]/[][]语法、浅拷贝内层共享 - 选型原则:规则数据用
[,](连续内存、速度快),不规则数据用[][](灵活、省空间)
七、入门练习题
声明练习:声明一个交错数组,外层长度 4,然后给每个内层分别分配长度 1、2、3、4,并填入数字 1~10。
遍历练习:用三种方式(嵌套 for、foreach、for+foreach)遍历以下数组并输出:
int[][] data = {
new int[] { 5, 10 },
new int[] { 15, 20, 25 },
new int[] { 30 }
};统计练习:计算上面数组中所有元素的总和、平均值、最大值。
修改练习:把上面数组中所有大于 15 的元素都减 5,输出修改后的数组。
综合练习:定义一个交错数组表示 4 个小组的分数:
{ new[]{80,90}, new[]{70,85,95}, new[]{60,75}, new[]{88} },找出:- 每个小组的平均分
- 全校最高分的位置(第几组第几人)
- 及格率最高的小组编号
思考题:下面代码输出什么?为什么?
int[][] a = { new int[] { 1, 2 } };
int[][] b = a;
b[0][0] = 999;
Console.WriteLine(a[0][0]);点击查看第 6 题答案
输出 999。
因为 b = a 是别名赋值,a 和 b 指向同一个外层数组,外层数组里的内层引用也相同。修改 b[0][0] 就是修改 a[0][0]。
如果想让 a 保持不变,需要逐行深拷贝:
int[][] b = new int[a.Length][];
for (int i = 0; i < a.Length; i++)
{
b[i] = (int[])a[i].Clone();
}掌握了交错数组的定义和基础使用,你已经能灵活处理各种不规则数据了。下一篇我们将学习交错数组的遍历和进阶方法,深入探索 LINQ 在交错数组上的高级用法,以及性能优化的实战技巧。