Skip to content

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]——一整块连续内存:

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字节)
└─────┴─────┴─────┘

交错数组 int[][]——外层存引用,内层分散各处:

text
逻辑结构(你看到的):         物理存储(内存中的样子):
┌─────────┐                   外层数组(连续):
│ 第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 为什么叫「交错」或「锯齿」 ​

因为每行长度不同,画出来像锯齿一样参差不齐:

text
int[][] jagged = {
    new[] { 1, 2 },           // 第0行:2个元素  ██
    new[] { 3, 4, 5, 6 },     // 第1行:4个元素  ████
    new[] { 7 }               // 第2行:1个元素  █
};
// 画出来就是:    ██
//                ████
//                █
// 像不像锯齿?所以叫 Jagged(锯齿状)

二、交错数组的声明与初始化全场景详解 ​

2.1 语法符号重点标注 ​

交错数组的语法有两个核心符号规则,新手极易混淆:

符号位置含义记忆口诀
[][]类型声明处「数组的数组」,两对独立方括号两对括号 = 交错
,多维数组中一个方括号里的逗号,分隔维度一个逗号 = 多维
[]访问时每对括号对应一层数组几层数组几对括号

对比记忆:

csharp
int[,] multi;    // 多维数组:一个方括号 + 逗号 → 访问用 arr[i, j]
int[][] jagged;  // 交错数组:两个独立方括号   → 访问用 arr[i][j]

2.2 声明方式一:只声明外层(最常用) ​

语法:数据类型[][] 数组名 = new 数据类型[外层长度][];

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

逐行拆解:

  1. int[][] classes = new int[3][]; —— 声明外层数组,长度 3,内层全是 null
  2. classes[0] = new int[2]; —— 给第 0 个内层数组分配空间,长度 2
  3. classes[1] = new int[4]; —— 第 1 个内层数组,长度 4(和第 0 个不同!)
  4. classes[0][0] = 90; —— 访问:第一对括号 [0] 定位到内层数组,第二对括号 [0] 定位到元素

⚠️ 关键提醒:new int[3][] 只创建了外层,内层全是 null。如果不 new 内层就直接访问,会抛 NullReferenceException!

2.3 声明方式二:声明时直接初始化(推荐) ​

语法:数据类型[][] 数组名 = { new 数据类型[]{...}, new 数据类型[]{...} };

csharp
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列)
    }
}

逐行拆解:

  1. new int[] { 90, 85 } —— 创建第 0 个内层数组,直接放入 2 个元素
  2. new int[] { 88, 92, 60, 71 } —— 第 1 个内层数组,4 个元素(长度不同!)
  3. new[] { 76 } —— 省略 int,编译器根据内容自动推断类型
  4. scores[1][2] —— 第一对 [1] 拿到第 1 个内层数组 {88, 92, 60, 71},第二对 [2] 拿到第 2 个元素 60

2.4 声明方式三:先声明后赋值(分步进行) ​

csharp
// 第一步:声明变量(此时是 null)
int[][] data;

// 第二步:创建外层数组
data = new int[2][];

// 第三步:创建内层数组
data[0] = new int[] { 1, 2, 3 };
data[1] = new int[] { 4, 5 };

2.5 三维交错数组 ​

csharp
// 三维交错:数组的数组的数组
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 元素访问与修改 ​

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

操作拆解:

  1. classes[1][2] —— 第一对括号 [1] 拿到 new int[] { 88, 92, 60, 71 },第二对括号 [2] 拿到 60
  2. classes[0][0] += 5 —— 先读出旧值 90,加 5,再写回原位置
  3. classes[0].Length —— 获取第 0 行的长度(2),classes[0].Length - 1 是该行最后一个元素的索引

3.2 数组长度获取 ​

csharp
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 循环(全能选手) ​

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

输出:

text
班级 0(2 人):90 85
班级 1(4 人):88 92 60 71
班级 2(1 人):76

关键:内层循环上界是 classes[i].Length(当前行的长度),不是固定值!

3.4 遍历方式二:foreach 循环(最省心) ​

csharp
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 混合(推荐) ​

csharp
// 外层 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 查询 ​

csharp
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 内层,直接访问 ​

错误代码:

csharp
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 个空货架,还没放隔板就想放东西。

修正:

csharp
int[][] jagged = new int[3][];
jagged[0] = new int[2];   // 先给第 0 个货架装隔板
jagged[0][0] = 1;         // 现在可以放东西了

4.2 坑二:遍历时用固定长度,导致越界 ​

错误代码:

csharp
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 行越界!
    }
}

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

csharp
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 坑三:混淆 [,] 和 [][] 的语法 ​

错误代码:

csharp
int[][] jagged = new int[2][];
jagged[0] = new int[3];

// jagged[0, 0] = 1;      // 编译错误!交错数组不能用逗号访问

int[,] multi = new int[2, 3];
// multi[0][0] = 1;       // 编译错误!多维数组不能用双括号访问

修正:记住对应关系——声明用什么,访问就用什么:

csharp
jagged[0][0] = 1;   // int[][] → 双括号
multi[0, 0] = 1;    // int[,] → 逗号

4.4 坑四:浅拷贝——Clone 后内层仍共享 ​

错误代码:

csharp
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() 只复制了外层数组,内层数组的引用被原样复制——两个外层数组指向同一批内层数组。

修正:逐行深拷贝:

csharp
int[][] copy = new int[original.Length][];
for (int i = 0; i < original.Length; i++)
{
    copy[i] = (int[])original[i].Clone();   // 每行单独复制
}

4.5 坑五:内层数组为 null 时直接遍历 ​

错误代码:

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

修正:遍历时先判空:

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

输出:

text
===== 各班级成绩统计 =====
班级 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

核心思路拆解:

  1. 交错数组存储:每个班级人数不同,用 int[][] 按需分配
  2. 嵌套 for 统计:外层走班级,内层走学生,逐班计算平均分、最高分、最低分、及格率
  3. SelectMany 摊平:把 3 个班级合成一个序列,用 LINQ 快速算全校统计
  4. 双重循环找最值:记录最高分的班级号和学号
  5. 索引修改:classes[i][j] += 5 直接修改原数组

六、知识点总结 ​

  1. 交错数组 = 数组的数组:外层存引用,内层分散,每行长度可以不同
  2. 语法规则:声明用 int[][],访问用 arr[i][j],与 int[,] 的 arr[i, j] 完全不同
  3. 初始化:new int[3][] 后必须逐行 new,否则内层是 null
  4. 遍历:外层 classes.Length,内层 classes[i].Length,几层数组几对括号
  5. 四大坑:忘 new 内层、固定长度遍历、混用 [,]/[][] 语法、浅拷贝内层共享
  6. 选型原则:规则数据用 [,](连续内存、速度快),不规则数据用 [][](灵活、省空间)

七、入门练习题 ​

  1. 声明练习:声明一个交错数组,外层长度 4,然后给每个内层分别分配长度 1、2、3、4,并填入数字 1~10。

  2. 遍历练习:用三种方式(嵌套 for、foreach、for+foreach)遍历以下数组并输出:

csharp
int[][] data = {
    new int[] { 5, 10 },
    new int[] { 15, 20, 25 },
    new int[] { 30 }
};
  1. 统计练习:计算上面数组中所有元素的总和、平均值、最大值。

  2. 修改练习:把上面数组中所有大于 15 的元素都减 5,输出修改后的数组。

  3. 综合练习:定义一个交错数组表示 4 个小组的分数:{ new[]{80,90}, new[]{70,85,95}, new[]{60,75}, new[]{88} },找出:

    • 每个小组的平均分
    • 全校最高分的位置(第几组第几人)
    • 及格率最高的小组编号
  4. 思考题:下面代码输出什么?为什么?

csharp
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 保持不变,需要逐行深拷贝:

csharp
int[][] b = new int[a.Length][];
for (int i = 0; i < a.Length; i++)
{
    b[i] = (int[])a[i].Clone();
}

掌握了交错数组的定义和基础使用,你已经能灵活处理各种不规则数据了。下一篇我们将学习交错数组的遍历和进阶方法,深入探索 LINQ 在交错数组上的高级用法,以及性能优化的实战技巧。