Skip to content

C# 数组的初始化和默认值:新柜子的「出厂设置」 ​

引言:新柜子里到底装了什么 ​

你去超市租了一排储物柜,还没放任何东西。这时候打开柜子,里面是什么?

  • 是「什么都没有」吗?
  • 还是有一团空气?
  • 或者贴着一张「空」的纸条?

在 C# 的世界里,这个问题有明确答案:数组一旦创建,每个位置都会被系统自动填入一个「出厂设置」——默认值。不同类型的柜子,出厂设置不同:

  • 装整数的柜子,出厂放的是 0
  • 装小数的柜子,出厂放的是 0.0
  • 装布尔的柜子,出厂放的是 false
  • 装字符串的柜子,出厂放的是 null(真空,连空气都没有)

理解「初始化方式」和「默认值机制」,是避免程序崩溃(尤其是臭名昭著的 NullReferenceException)的关键。本文带你彻底搞懂这两个话题。


一、入门引导:数组基础与两大阵营 ​

1.1 数组基础(30 秒回顾) ​

数组 = 一排固定长度、类型统一、内存连续的储物柜,通过从 0 开始的索引访问。

csharp
int[] scores = new int[5];   // 创建一排能装 5 个整数的柜子
scores[0] = 85;              // 第 1 个柜子放 85
Console.WriteLine(scores[1]); // 第 2 个柜子没放过东西,输出什么?→ 0(默认值)

1.2 两大阵营:值类型 vs 引用类型 ​

C# 的所有数据类型分两大阵营,它们的默认值规则完全不同,这是本文的核心前提:

阵营成员变量里装的是什么生活类比
值类型int、double、bool、char、struct 等数据本身柜子里直接放物品
引用类型string、自定义 class、数组本身等数据的「地址」(门牌号)柜子里放一张写着地址的纸条

为什么要区分? 因为它们的默认值完全不同:

  • 值类型的柜子,出厂时放着该类型的「零值」(0、false 等),拿来就能用
  • 引用类型的柜子,出厂时纸条是空白的(null),直接用会崩溃
csharp
int[] numbers = new int[3];
Console.WriteLine(numbers[0]);     // 输出:0(安全)

string[] names = new string[3];
Console.WriteLine(names[0] == null); // 输出:True
// Console.WriteLine(names[0].Length); // 崩溃!NullReferenceException

1.3 底层原理极简版:默认值从哪来 ​

当你写下 new int[5] 时,.NET 运行时会做两件事:

  1. 在堆(Heap)内存中开辟一块连续空间,大小 = 5 × 4 字节(一个 int 占 4 字节)
  2. 把这块内存全部清零(所有二进制位设为 0)

这就是默认值的来源——内存清零。对值类型来说,全 0 恰好就是 0、0.0、false;对引用类型来说,全 0 表示「空地址」,也就是 null。

所以默认值不是编译器「好心送你」的,而是内存分配时的自然结果。这也解释了为什么默认值是自动的、零开销的——清零内存本来就是分配内存的必要步骤。


二、数组初始化方式大全 ​

2.1 方式一:指定长度的基础初始化 ​

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

是什么:只告诉编译器「要几个柜子」,不放东西,每个位置自动填默认值。

csharp
using System;

class Program
{
    static void Main()
    {
        // 声明一个能装 5 个 int 的数组,所有位置默认是 0
        int[] scores = new int[5];
        Console.WriteLine(scores[0]);   // 输出:0
        Console.WriteLine(scores[4]);   // 输出:0

        // 后续通过索引逐个赋值
        scores[0] = 85;
        scores[1] = 92;
        Console.WriteLine(scores[0]);   // 输出:85
    }
}

什么时候用:

  • ✅ 事先不知道具体值,只知道数量(如「接收用户输入的 10 个数」)
  • ✅ 作为缓冲区、缓存数组(先占位,后填数据)

版本兼容:C# 1.0 起可用,所有版本通用。

2.2 方式二:声明同时赋值的简化初始化 ​

语法:数据类型[] 数组名 = { 值1, 值2, ... };(完整写法是 new 数据类型[] { ... })

是什么:建柜子的同时把东西放好,长度由元素个数自动决定。

csharp
using System;

class Program
{
    static void Main()
    {
        // 完整写法
        int[] scores = new int[] { 85, 92, 78, 60, 95 };

        // 简化写法(推荐):省略 new int[]
        string[] names = { "张三", "李四", "王五" };

        // 甚至可以同时指定长度(长度必须和元素个数一致)
        int[] ages = new int[3] { 18, 20, 22 };

        Console.WriteLine(scores.Length);   // 输出:5
        Console.WriteLine(names[1]);        // 输出:李四
    }
}

什么时候用:

  • ✅ 写代码时就知道所有元素(配置项、固定选项、测试数据)

常见坑点:

csharp
int[] a;
// a = { 1, 2, 3 };          // 编译错误!简写只能在声明同一行使用
a = new int[] { 1, 2, 3 };   // 正确:分开写时必须带上 new int[]

// int[] b = new int[3] { 1, 2 };  // 编译错误!声明长度 3 却只给了 2 个值

版本兼容:C# 1.0 起可用。

2.3 方式三:隐式类型数组初始化 ​

语法:var 数组名 = new[] { 值1, 值2, ... };

是什么:让编译器根据元素自动推断数组类型。

csharp
using System;

class Program
{
    static void Main()
    {
        var scores = new[] { 85, 92, 78 };       // 推断为 int[]
        var prices = new[] { 9.9, 19.5, 3.0 };   // 推断为 double[]
        var names = new[] { "张三", "李四" };     // 推断为 string[]

        Console.WriteLine(scores.GetType());      // 输出:System.Int32[]

        // 类型混合时,编译器会找「共同祖先」
        var mixed = new[] { 1, 2.5 };             // int 和 double → 推断为 double[]

        // var empty = new[] { };                 // 编译错误!没有元素,无法推断
        // var conflict = new[] { 1, "abc" };     // 编译错误!int 和 string 没有共同类型
    }
}

什么时候用:

  • ✅ 局部变量,想让代码更简洁
  • ✅ 配合匿名类型(new[] { new { Name = "张三", Age = 18 } })

版本兼容:var 关键字 C# 3.0 引入。

2.4 方式四:多维数组的初始化 ​

是什么:二维数组像 Excel 表格(行 × 列),三维数组像一摞表格。注意和后面的「交错数组」区分。

csharp
using System;

class Program
{
    static void Main()
    {
        // ===== 二维数组 =====
        // 1. 只指定维度
        int[,] grid = new int[2, 3];   // 2 行 3 列,所有元素默认 0

        // 2. 声明同时赋值(嵌套大括号)
        int[,] matrix = {
            { 1, 2, 3 },   // 第 0 行
            { 4, 5, 6 }    // 第 1 行
        };

        // 3. 完整写法
        int[,] table = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };

        Console.WriteLine(matrix[1, 2]);      // 输出:6(第 1 行第 2 列)
        Console.WriteLine(matrix.GetLength(0)); // 输出:2(行数)
        Console.WriteLine(matrix.GetLength(1)); // 输出:3(列数)

        // ===== 三维数组 =====
        int[,,] cube = new int[2, 3, 4];   // 2 层 × 3 行 × 4 列
        int[,,] data = {
            { { 1, 2 }, { 3, 4 } },
            { { 5, 6 }, { 7, 8 } }
        };   // 2 层 × 2 行 × 2 列

        Console.WriteLine(data[1, 0, 1]);   // 输出:6
    }
}

关键点:

  • 多维数组每个维度用逗号分隔:[,] 是二维,[,,] 是三维
  • 每一维的长度固定,整个数组是一个矩形(每行长度必须相同)
  • 用 GetLength(维度) 获取某一维的长度(维度从 0 开始)
  • 默认值规则和一维数组一样:数值型全 0,引用型全 null

什么时候用:棋盘、地图网格、像素矩阵、报表数据。

版本兼容:C# 1.0 起可用。

2.5 方式五:交错数组(锯齿数组)的初始化 ​

是什么:「数组的数组」——外层数组的每个元素本身就是一个数组,且每个内层数组长度可以不同(像锯齿一样参差不齐)。

csharp
using System;

class Program
{
    static void Main()
    {
        // ===== 1. 分步初始化(最常用)=====
        // 第一步:创建外层数组(3 个位置,每个位置将来放一个 int[])
        int[][] jagged = new int[3][];
        // 此时 jagged[0]、jagged[1]、jagged[2] 都是 null!

        // 第二步:逐个创建内层数组
        jagged[0] = new int[] { 1, 2 };          // 第 0 行 2 个元素
        jagged[1] = new int[] { 3, 4, 5, 6 };    // 第 1 行 4 个元素
        jagged[2] = new int[] { 7 };             // 第 2 行 1 个元素

        // ===== 2. 统一初始化(声明时全给值)=====
        int[][] jagged2 = new int[][]
        {
            new int[] { 1, 2 },
            new int[] { 3, 4, 5 },
            new int[] { 6, 7, 8, 9 }
        };

        // 简化写法(C# 3.0+)
        int[][] jagged3 =
        {
            new[] { 1, 2 },
            new[] { 3, 4, 5 }
        };

        Console.WriteLine(jagged[1][2]);       // 输出:5(第 1 行第 2 个)
        Console.WriteLine(jagged.Length);      // 输出:3(外层长度)
        Console.WriteLine(jagged[1].Length);   // 输出:4(第 1 行的长度)
    }
}

关键点:

  • 声明语法是 int[][](两对独立方括号),不是 int[,]
  • new int[3][] 只创建了外层数组,内层全是 null,必须逐个 new
  • 每个内层数组长度随意,适合「每行数据量不同」的场景

多维 vs 交错,一图看懂:

text
二维数组 int[2,3](矩形):      交错数组 int[][](锯齿):
┌───┬───┬───┐                  ┌───┬───┐
│ 1 │ 2 │ 3 │  第0行            │ 1 │ 2 │              第0行(2个)
├───┼───┼───┤                  ├───┼───┼───┬───┐
│ 4 │ 5 │ 6 │  第1行            │ 3 │ 4 │ 5 │ 6 │       第1行(4个)
└───┴───┴───┘                  └───┴───┴───┴───┘
每行必须等长                      每行长度随意

什么时候用:班级成绩表(每个班人数不同)、文件路径分组、不规则数据。

版本兼容:C# 1.0 起可用。

2.6 方式六:集合初始化器在数组中的应用 ​

是什么:利用 new 配合方法返回数组,实现更灵活的初始化。

csharp
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // 1. 用 Enumerable.Range 生成连续序列
        int[] numbers = Enumerable.Range(1, 10).ToArray();
        // 结果:{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }

        // 2. 用 Enumerable.Repeat 生成重复值
        int[] zeros = Enumerable.Repeat(0, 5).ToArray();
        // 结果:{ 0, 0, 0, 0, 0 }

        // 3. 用 LINQ 从现有数据筛选生成新数组
        int[] scores = { 85, 42, 91, 38, 76 };
        int[] passed = scores.Where(s => s >= 60).ToArray();
        // 结果:{ 85, 91, 76 }

        // 4. 用 Enumerable 生成计算序列
        int[] squares = Enumerable.Range(1, 5).Select(x => x * x).ToArray();
        // 结果:{ 1, 4, 9, 16, 25 }

        Console.WriteLine(string.Join(", ", numbers));  // 输出:1, 2, 3, 4, 5, 6, 7, 8, 9, 10
        Console.WriteLine(string.Join(", ", squares));  // 输出:1, 4, 9, 16, 25
    }
}

什么时候用:

  • ✅ 批量生成规则数据(序号、测试数据、占位数组)
  • ✅ 从现有集合筛选/转换生成新数组

版本兼容:Enumerable 系列方法 C# 3.0(LINQ)起可用。

2.7 方式七:新版 C# 的初始化语法糖 ​

2.7.1 集合表达式(C# 12,最推荐的新写法) ​

C# 12(.NET 8,2023 年 11 月发布)引入了集合表达式,用 [...] 统一初始化各种集合:

csharp
using System;

class Program
{
    static void Main()
    {
        // C# 12 新语法:方括号初始化
        int[] scores = [85, 92, 78, 60, 95];
        string[] names = ["张三", "李四"];

        // 空数组
        int[] empty = [];

        // 支持展开运算符(..)合并数组
        int[] a = [1, 2, 3];
        int[] b = [4, 5, 6];
        int[] combined = [..a, ..b];        // { 1, 2, 3, 4, 5, 6 }
        int[] withExtra = [0, ..a, 99];     // { 0, 1, 2, 3, 99 }

        Console.WriteLine(scores[0]);       // 输出:85
        Console.WriteLine(combined.Length); // 输出:6
        Console.WriteLine(string.Join(",", withExtra)); // 输出:0,1,2,3,99
    }
}

优势:语法更短、可与其他集合类型(List<T>、Span<T>)统一写法、支持展开合并。

版本兼容:⚠️ 仅 C# 12 / .NET 8+。旧项目用传统写法即可,两者完全等价。

2.7.2 索引与范围运算符配合初始化(C# 8) ​

C# 8 引入的 ^(从末尾数索引)和 ..(范围)运算符,常用于从已有数组切片创建新数组:

csharp
using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        // ^ 运算符:^1 是最后一个元素,^2 是倒数第二个
        Console.WriteLine(numbers[^1]);   // 输出:9
        Console.WriteLine(numbers[^2]);   // 输出:8

        // .. 范围运算符:切片生成新数组
        int[] first3 = numbers[0..3];     // { 0, 1, 2 }(含头不含尾)
        int[] last3 = numbers[^3..];      // { 7, 8, 9 }(从倒数第3个到末尾)
        int[] middle = numbers[2..5];     // { 2, 3, 4 }
        int[] all = numbers[..];          // 完整拷贝一份(相当于 Clone)

        Console.WriteLine(string.Join(",", first3)); // 输出:0,1,2
        Console.WriteLine(string.Join(",", last3));  // 输出:7,8,9
    }
}

版本兼容:⚠️ ^ 和 .. 运算符需要 C# 8 / .NET Core 3.0+。

2.7.3 空合并赋值配合懒初始化(C# 8) ​

??= 运算符可以实现「为 null 才初始化」的懒加载模式:

csharp
using System;

class Program
{
    // 模拟一个缓存字段,初始为 null
    static string[]? _cache;

    static string[] GetData()
    {
        // ??= :如果左边是 null,才执行右边的赋值;否则什么都不做
        _cache ??= new string[] { "缓存数据1", "缓存数据2" };
        return _cache;
    }

    static void Main()
    {
        Console.WriteLine(_cache == null);   // 输出:True(还没初始化)

        string[] data1 = GetData();          // 第一次调用:执行初始化
        Console.WriteLine(_cache.Length);    // 输出:2

        string[] data2 = GetData();          // 第二次调用:直接返回已有数组
        Console.WriteLine(ReferenceEquals(data1, data2));  // 输出:True(同一个数组)
    }
}

什么时候用:缓存、单例集合、延迟加载——避免重复创建大数组。

版本兼容:⚠️ ??= 需要 C# 8+;string[]?(可空引用类型标注)需要 C# 8+。

2.8 初始化方式速查表 ​

方式语法示例适用场景版本要求
指定长度new int[5]只知数量不知值C# 1.0
声明即赋值{ 1, 2, 3 }已知所有元素C# 1.0
隐式类型var a = new[] { 1, 2 }局部变量、求简洁C# 3.0
多维数组new int[2,3] 或 { {1,2}, {3,4} }矩阵、表格C# 1.0
交错数组new int[3][] + 逐个 new不规则数据C# 1.0
Enumerable 生成Range(1,10).ToArray()批量规则数据C# 3.0
集合表达式[1, 2, 3]统一简洁写法C# 12
切片arr[1..4]从已有数组截取C# 8
懒初始化_cache ??= new int[10]缓存、延迟加载C# 8

三、核心章节:数组默认值机制深度解析 ​

3.1 值类型数组的默认值 ​

规则:所有值类型数组的元素,默认被初始化为该类型的「零值」——即内存全 0 对应的值。

csharp
using System;

class Program
{
    static void Main()
    {
        int[] ints = new int[3];
        Console.WriteLine(ints[0]);         // 输出:0

        double[] doubles = new double[3];
        Console.WriteLine(doubles[0]);      // 输出:0

        bool[] bools = new bool[3];
        Console.WriteLine(bools[0]);        // 输出:False

        char[] chars = new char[3];
        Console.WriteLine(chars[0] == '\0'); // 输出:True(空字符,不可见)
        Console.WriteLine((int)chars[0]);   // 输出:0(空字符的 Unicode 码就是 0)

        DateTime[] dates = new DateTime[1];
        Console.WriteLine(dates[0]);        // 输出:0001/1/1 0:00:00(DateTime 的零值)
    }
}

常见值类型默认值一览:

类型默认值说明
int / long / short / byte0整数家族全是 0
float / double / decimal0.0浮点家族全是 0.0
boolfalse
char'\0'(空字符)Unicode 码为 0 的字符
DateTime0001/1/1 00:00:00DateTime.MinValue
枚举值为 0 的成员若枚举没有 0 值成员,则为数值 0(可能无对应名称!)

枚举默认值的坑:

csharp
enum Color { Red = 1, Green = 2, Blue = 3 }   // 注意:没有 0 值成员!

Color[] colors = new Color[2];
Console.WriteLine(colors[0]);          // 输出:0(不是 Red!)
Console.WriteLine((int)colors[0]);     // 输出:0
// 默认值是数值 0,但 Color 枚举里没有定义 0 对应的名称——打印出来就是 "0"

避坑建议:定义枚举时,最好显式定义一个 None = 0 成员作为默认值,避免「幽灵值」。

结构体(struct)数组的默认值 ​

csharp
using System;

// 自定义结构体(值类型)
struct Point
{
    public int X;
    public int Y;
    public string Label;   // 结构体里也可以有引用类型成员!
}

class Program
{
    static void Main()
    {
        Point[] points = new Point[2];

        // 结构体数组的每个元素:值类型成员归零,引用类型成员为 null
        Console.WriteLine(points[0].X);         // 输出:0
        Console.WriteLine(points[0].Y);         // 输出:0
        Console.WriteLine(points[0].Label == null);  // 输出:True(注意!)

        // 危险:对 Label 调用方法会崩溃
        // Console.WriteLine(points[0].Label.Length);  // NullReferenceException!
    }
}

关键认知:结构体是值类型,但它的引用类型成员默认值依然是 null。「值类型数组绝对安全」的说法不严谨——结构体内部的引用成员照样能引发空引用。

3.2 引用类型数组的默认值 ​

规则:所有引用类型数组的元素,默认都是 null。

csharp
using System;

class Student
{
    public string Name = "";
    public int Score;
}

class Program
{
    static void Main()
    {
        // 字符串数组
        string[] names = new string[3];
        Console.WriteLine(names[0] == null);   // 输出:True

        // 自定义类数组
        Student[] students = new Student[5];
        Console.WriteLine(students[0] == null); // 输出:True

        // 数组的数组(数组本身也是引用类型!)
        int[][] jagged = new int[3][];
        Console.WriteLine(jagged[0] == null);   // 输出:True(这就是交错数组要逐个 new 的原因)

        // object 数组
        object[] objects = new object[2];
        Console.WriteLine(objects[1] == null);  // 输出:True
    }
}

为什么 null 这么危险?

null 表示「这个位置连门牌号都没有」。对 null 调用任何成员(方法、属性、索引器),程序立刻崩溃:

csharp
string[] names = new string[3];

// 三种典型死法:
// names[0].Length;              // NullReferenceException
// names[0].ToUpper();           // NullReferenceException
// if (names[0].StartsWith("张")) // NullReferenceException

防御性写法:

csharp
string[] names = new string[3];

// 方式 1:先判空(传统稳妥)
if (names[0] != null)
{
    Console.WriteLine(names[0].Length);
}

// 方式 2:空条件运算符 ?.(C# 6+,更简洁)
Console.WriteLine(names[0]?.Length);   // names[0] 为 null 时整体返回 null,不崩溃

// 方式 3:空合并运算符 ??(C# 2+,提供兜底值)
Console.WriteLine(names[0] ?? "(未填写)");   // 为 null 时输出「(未填写)」

3.3 可空值类型数组的默认值 ​

是什么:可空值类型(int?、double?、bool?……)是「可以为 null 的值类型」,语法是在值类型后加 ?。

核心差异:

类型默认值能否表示「没有值」
int0❌(0 就是一个普通的数)
int?null✅(null 明确表示「没有数据」)
csharp
using System;

class Program
{
    static void Main()
    {
        // 普通 int 数组:默认 0,无法区分「没填」和「考了 0 分」
        int[] scores = new int[3];
        Console.WriteLine(scores[0]);            // 输出:0

        // 可空 int 数组:默认 null,明确表示「还没录入成绩」
        int?[] nullableScores = new int?[3];
        Console.WriteLine(nullableScores[0] == null);   // 输出:True
        Console.WriteLine(nullableScores[0].HasValue);  // 输出:False(HasValue 判断是否有值)

        // 赋值后
        nullableScores[0] = 85;
        Console.WriteLine(nullableScores[0].HasValue);  // 输出:True
        Console.WriteLine(nullableScores[0].Value);     // 输出:85(.Value 取出真实值)

        // 安全取值的三种姿势
        int? score = nullableScores[1];   // null
        Console.WriteLine(score ?? 0);            // 输出:0(为 null 时用 0 兜底)
        Console.WriteLine(score.GetValueOrDefault(-1)); // 输出:-1(指定兜底值)
    }
}

什么时候用可空值类型:

  • ✅ 数据库映射(数据库字段允许 NULL)
  • ✅ 表单数据(用户可能没填某项)
  • ✅ 需要区分「零值」和「没有值」的场景(0 分 vs 未参加考试)

3.4 自定义默认值:覆盖系统默认 ​

有时候系统默认值不够用——比如你希望 string 数组默认是 ""(空串)而不是 null。实现方法:

csharp
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // 方法 1:for 循环填充(最直观)
        string[] names = new string[5];
        for (int i = 0; i < names.Length; i++)
        {
            names[i] = "";   // 用空串替代 null
        }

        // 方法 2:Array.Fill(.NET Core 2.0+ / .NET Standard 2.1+,一行搞定)
        string[] names2 = new string[5];
        Array.Fill(names2, "");
        // 也可以只填充一部分:Array.Fill(names2, "待定", 0, 3);(前 3 个填「待定」)

        // 方法 3:Enumerable.Repeat 生成(C# 3.0+)
        string[] names3 = Enumerable.Repeat("", 5).ToArray();
        int[] defaultScores = Enumerable.Repeat(-1, 10).ToArray();  // 10 个 -1

        // 方法 4:对象数组——每个位置放一个独立对象(注意!)
        // 错误示范:所有位置指向同一个对象
        var sb = new System.Text.StringBuilder();
        var arr1 = Enumerable.Repeat(sb, 3).ToArray();  // 3 个位置都是同一个 StringBuilder!

        // 正确示范:每个位置独立 new
        var arr2 = Enumerable.Range(0, 3)
                             .Select(_ => new System.Text.StringBuilder())
                             .ToArray();  // 3 个位置是 3 个独立对象

        Console.WriteLine(names2[0] == "");   // 输出:True
    }
}

⚠️ 重要坑点:给引用类型数组填默认对象时,Array.Fill(arr, obj) 和 Enumerable.Repeat(obj, n) 会让所有位置指向同一个对象(浅拷贝陷阱),改一个等于改全部。需要独立对象时,用 Select(_ => new ...) 每个位置单独创建。

3.5 默认值规则总结表 ​

数组类型元素默认值可直接使用典型风险
int[]、double[] 等数值型0 / 0.0✅无法区分「没填」和「真 0」
bool[]false✅无法区分「没填」和「真 false」
char[]'\0'✅空字符打印不可见,易被误认为「空的」
DateTime[]0001/1/1✅这个日期几乎不可能是真实业务值
枚举数组数值 0 对应成员⚠️枚举没定义 0 成员时出现「幽灵值」
string[]null❌NullReferenceException 高发区
自定义类数组null❌同上
结构体数组成员各自归零 / null⚠️结构体内的引用成员仍是 null
int?[] 等可空型null⚠️用 .Value 前必须查 .HasValue
int[][](交错数组)内层全 null❌忘记 new 内层数组直接访问即崩

四、常见误区排查 ​

4.1 误区一:以为引用类型数组「自动有元素」 ​

错误代码:

csharp
class Student
{
    public string Name = "";
    public int Score;
}

Student[] students = new Student[3];
students[0].Name = "张三";   // 运行到这一行,直接崩溃!

错误现象:System.NullReferenceException: Object reference not set to an instance of an object.(未将对象引用设置到对象的实例)

原因分析:new Student[3] 只创建了「3 个空位置」,每个位置是 null,并没有自动创建 3 个 Student 对象。就像你租了 3 个柜子,但柜子里连张纸条都没有,你却想往「柜子里的学生档案」上写名字。

正确代码:

csharp
Student[] students = new Student[3];

// 先创建对象放进柜子,再使用
students[0] = new Student();
students[0].Name = "张三";   // 现在安全了

// 或者用循环一次性全部初始化
for (int i = 0; i < students.Length; i++)
{
    students[i] = new Student();
}

4.2 误区二:交错数组忘记初始化内层 ​

错误代码:

csharp
int[][] jagged = new int[2][];
jagged[0][0] = 100;   // 崩溃!jagged[0] 是 null,null 不能按下标访问

原因分析:new int[2][] 只建了外层数组,jagged[0] 和 jagged[1] 都是 null。

正确代码:

csharp
int[][] jagged = new int[2][];
jagged[0] = new int[3];    // 先给内层数组「安柜子」
jagged[0][0] = 100;        // 再放东西

4.3 误区三:混淆多维数组和交错数组的语法 ​

错误代码:

csharp
int[,] grid = new int[2][];   // 编译错误![,] 是二维数组,后面不能用 [][]

int[][] jagged = new int[2,3]; // 编译错误![][] 是交错数组,不能用 [,] 的语法

int[,] data = { 1, 2, 3 };     // 编译错误!二维数组必须用嵌套大括号

正确代码:

csharp
// 二维数组:一对括号里用逗号
int[,] grid = new int[2, 3];
int[,] data = { { 1, 2, 3 }, { 4, 5, 6 } };

// 交错数组:两对独立括号
int[][] jagged = new int[2][];
jagged[0] = new int[] { 1, 2, 3 };
jagged[1] = new int[] { 4, 5 };

记忆口诀:逗号在括号里是多维([,]),括号叠括号是交错([][])。

4.4 误区四:用「重复引用」填充对象数组 ​

错误代码:

csharp
class Counter { public int Value; }

Counter[] counters = new Counter[3];
Counter shared = new Counter();
Array.Fill(counters, shared);   // 3 个位置指向同一个对象!

counters[0].Value = 100;
Console.WriteLine(counters[1].Value);   // 输出:100(意外!counters[1] 也变了)

原因分析:Array.Fill 填充的是引用,3 个位置指向同一个 Counter 对象。改任意一个,其他位置看到的也跟着变——又浅拷贝陷阱。

正确代码:

csharp
Counter[] counters = new Counter[3];
for (int i = 0; i < counters.Length; i++)
{
    counters[i] = new Counter();   // 每个位置独立 new
}

counters[0].Value = 100;
Console.WriteLine(counters[1].Value);   // 输出:0(互不影响)

4.5 误区五:用默认值判断「有没有数据」 ​

错误代码:

csharp
// 想统计「录入成绩的学生人数」,0 分表示没录入
int[] scores = new int[5];
scores[0] = 85;
scores[1] = 0;    // 这位同学真的考了 0 分!
scores[2] = 92;

int count = 0;
foreach (int s in scores)
{
    if (s != 0) count++;   // 把考 0 分的同学漏掉了!
}
Console.WriteLine(count);   // 输出:2,但真实录入人数应该是 3

原因分析:int 的默认值是 0,而 0 又可能是真实数据,两者无法区分。

正确代码:用可空类型明确表示「未录入」:

csharp
int?[] scores = new int?[5];
scores[0] = 85;
scores[1] = 0;      // 真实 0 分,HasValue 是 true
scores[2] = 92;
// scores[3]、scores[4] 保持 null = 未录入

int count = scores.Count(s => s.HasValue);   // 需要 using System.Linq;
Console.WriteLine(count);   // 输出:3(正确统计)

4.6 误区六:切片范围越界 ​

错误代码:

csharp
int[] numbers = { 1, 2, 3 };

// int[] slice = numbers[0..5];   // 运行时异常!数组只有 3 个元素
// Console.WriteLine(numbers[^4]); // 运行时异常!倒数第 4 个不存在

原因分析:C# 8 的 ^ 和 .. 并不豁免越界检查,范围超出数组长度照样抛 IndexOutOfRangeException。

正确代码:

csharp
int[] numbers = { 1, 2, 3 };

// 切片前先确认范围合法
int take = Math.Min(5, numbers.Length);   // 取不超过长度的数量
int[] slice = numbers[0..take];           // 安全:{ 1, 2, 3 }
Console.WriteLine(string.Join(",", slice));

五、实战应用 ​

5.1 场景一:批量初始化配置数据 ​

需求:系统启动时初始化一周 7 天的营业状态,默认全部「营业中」,周末单独设置。

csharp
using System;

enum BusinessStatus
{
    Closed = 0,    // 注意:0 值成员放最前,作为默认值
    Open = 1
}

class Program
{
    static void Main()
    {
        // 最佳实践:枚举定义 0 值成员,让默认值有意义
        BusinessStatus[] weekStatus = new BusinessStatus[7];   // 默认全是 Closed

        // 工作日设为营业
        for (int i = 0; i < 5; i++)
        {
            weekStatus[i] = BusinessStatus.Open;
        }

        // 或用 Array.Fill 填充一段范围
        Array.Fill(weekStatus, BusinessStatus.Open, 0, 5);

        string[] dayNames = { "周一", "周二", "周三", "周四", "周五", "周六", "周日" };
        for (int i = 0; i < 7; i++)
        {
            string status = weekStatus[i] == BusinessStatus.Open ? "营业中" : "休息";
            Console.WriteLine($"{dayNames[i]}:{status}");
        }
    }
}

输出:

text
周一:营业中
周二:营业中
周三:营业中
周四:营业中
周五:营业中
周六:休息
周日:休息

最佳实践:

  1. 枚举显式定义 0 值成员,让数组默认值有明确业务含义
  2. Array.Fill(arr, value, startIndex, count) 可以只填充指定范围,比循环更简洁

5.2 场景二:接口参数数组的防御性处理 ​

需求:编写一个接收学生名单的方法,要妥善处理各种「不靠谱」的传入。

csharp
using System;
using System.Linq;

class Program
{
    // 打印学生名单,需要防御 null 数组和 null 元素
    static void PrintStudents(string[]? students)
    {
        // 防御 1:整个数组是 null(调用方传了 null)
        if (students == null || students.Length == 0)
        {
            Console.WriteLine("名单为空");
            return;
        }

        // 防御 2:数组里混着 null 元素
        int valid = 0;
        for (int i = 0; i < students.Length; i++)
        {
            // 用 ?? 给 null 元素一个兜底显示值
            Console.WriteLine($"学生 {i + 1}:{students[i] ?? "(未命名)"}");
            if (students[i] != null) valid++;
        }
        Console.WriteLine($"有效名单:{valid} 人");
    }

    static void Main()
    {
        // 模拟各种调用方
        PrintStudents(null);                                    // 传 null
        PrintStudents(new string[0]);                           // 传空数组
        PrintStudents(new string[] { "张三", null, "李四" });   // 混着 null 元素
    }
}

输出:

text
名单为空
名单为空
学生 1:张三
学生 2:(未命名)
学生 3:李四
有效名单:2 人

最佳实践:

  1. 公共方法的数组参数,永远先判 null 再判 Length
  2. 遍历引用类型数组时,对元素做 ?? 兜底,防止个别 null 炸掉整个循环
  3. 参数标注 string[]?(C# 8 可空引用类型),明确告诉调用方「这里可以传 null」

5.3 场景三:缓存数组的懒初始化 ​

需求:一个开销很大的数据数组,只在第一次使用时才创建,之后复用。

csharp
using System;

class DataService
{
    // 缓存字段:初始为 null,不占用内存
    private double[]? _priceCache;

    // 模拟一个开销很大的初始化过程
    private double[] LoadPrices()
    {
        Console.WriteLine("(正在从数据库加载价格数据...)");
        return new double[] { 19.9, 29.9, 39.9, 49.9 };
    }

    // 对外暴露的获取方法:懒加载
    public double[] GetPrices()
    {
        // ??= :第一次调用时才创建,之后直接复用
        _priceCache ??= LoadPrices();

        // 返回副本,防止外部修改污染缓存!
        return (double[])_priceCache.Clone();
    }
}

class Program
{
    static void Main()
    {
        var service = new DataService();

        Console.WriteLine("服务已创建,缓存还没加载");

        double[] prices1 = service.GetPrices();   // 第一次:触发加载
        Console.WriteLine($"拿到 {prices1.Length} 条价格");

        double[] prices2 = service.GetPrices();   // 第二次:直接用缓存
        Console.WriteLine($"又拿到 {prices2.Length} 条价格");

        // 外部修改不影响缓存
        prices1[0] = 9999;
        double[] prices3 = service.GetPrices();
        Console.WriteLine($"缓存第一条仍是:{prices3[0]}");   // 输出:19.9
    }
}

输出:

text
服务已创建,缓存还没加载
(正在从数据库加载价格数据...)
拿到 4 条价格
又拿到 4 条价格
缓存第一条仍是:19.9

最佳实践:

  1. 用 ??= 实现懒加载,避免程序启动时就创建大数组
  2. 对外返回 Clone() 副本而不是缓存本身——否则调用方一个 Sort 就把你的缓存污染了
  3. 缓存字段用可空标注 double[]?,语义清晰

六、知识点速查表 ​

6.1 初始化语法速查 ​

csharp
// ==== 一维数组 ====
int[] a = new int[5];                    // 定长,默认全 0
int[] b = { 1, 2, 3 };                   // 声明即赋值
int[] c = new int[] { 1, 2, 3 };         // 完整写法
var d = new[] { 1, 2, 3 };               // 隐式类型(C# 3.0)
int[] e = [1, 2, 3];                     // 集合表达式(C# 12)
int[] f = Enumerable.Range(1, 10).ToArray();  // 生成 1~10(C# 3.0)

// ==== 多维数组 ====
int[,] g = new int[2, 3];                // 2 行 3 列,默认全 0
int[,] h = { { 1, 2 }, { 3, 4 } };       // 声明即赋值
g.GetLength(0);                          // 行数;g.GetLength(1) 列数

// ==== 交错数组 ====
int[][] i = new int[3][];                // 只建外层,内层全 null!
i[0] = new int[] { 1, 2 };               // 内层必须逐个 new

// ==== 切片与索引(C# 8)====
int[] j = b[0..2];                       // { 1, 2 }(含头不含尾)
int last = b[^1];                        // 最后一个元素

// ==== 填充与懒加载 ====
Array.Fill(a, -1);                       // 全部填 -1
Array.Fill(a, -1, 0, 3);                 // 只填前 3 个
_cache ??= new int[10];                  // 为 null 才创建(C# 8)

6.2 默认值速查 ​

数组类型默认值安全使用
数值型(int/double 等)0 / 0.0✅ 直接用
boolfalse✅ 直接用
char'\0'✅ 直接用
DateTime0001/1/1✅ 直接用
枚举数值 0 的成员⚠️ 建议定义 None = 0
string / 自定义类null❌ 必须先判空或赋值
结构体成员各自默认⚠️ 结构体内的引用成员是 null
可空值类型 int?null⚠️ 用 .HasValue 判断
交错数组内层null❌ 必须先 new 内层

6.3 避坑清单 ​

  1. 引用类型数组创建后全是 null——用之前先判空(?.、??)或逐个 new
  2. 交错数组要先 new 内层——new int[3][] 后内层全是 null
  3. [,] 和 [][] 别搞混——逗号在括号里是多维,括号叠括号是交错
  4. Array.Fill / Repeat 填对象是浅拷贝——需要独立对象用循环或 Select(_ => new ...)
  5. 默认值 0 无法区分「没数据」——用 int? 可空类型
  6. 切片不豁免越界——arr[0..n] 的 n 不能超过长度
  7. 对外暴露数组先 Clone——防止调用方污染你的内部数据

七、入门练习题 ​

  1. 基础初始化:分别用三种方式创建一个包含 5 个科目名称的 string 数组(指定长度后逐个赋值、声明即赋值、隐式类型)。

  2. 默认值判断:创建 int[]、bool[]、string[]、DateTime[] 各一个(长度 3),不写任何赋值语句,直接输出每个数组的第一个元素,验证默认值。

  3. 多维初始化:创建一个 3 行 4 列的二维数组,用嵌套循环填入「行号 × 列号」的值,然后按表格形式打印出来。

  4. 交错数组:创建一个交错数组表示 3 个小组的成员名单(第 1 组 3 人、第 2 组 5 人、第 3 组 2 人),打印每个小组的人数和成员。

  5. 可空类型:用 int?[] 表示 5 名学生的成绩,其中 2 人未录入(保持 null),统计已录入人数和已录入成绩的平均分(提示:用 Where(s => s.HasValue))。

  6. 切片练习(C# 8+):对数组 {10, 20, 30, 40, 50, 60} 分别取出:前 3 个、后 2 个、中间第 2~4 个,并验证原数组未被修改。

  7. 思考题:下面代码的输出是什么?为什么?

csharp
var list = new System.Text.StringBuilder("A");
var arr = Enumerable.Repeat(list, 3).ToArray();
arr[0].Append("B");
Console.WriteLine(arr[1].ToString());
点击查看第 7 题答案

输出 AB。

因为 Enumerable.Repeat(list, 3) 把同一个 StringBuilder 对象的引用重复填了 3 次,arr[0]、arr[1]、arr[2] 指向同一个对象。通过 arr[0] 追加 "B",arr[1] 看到的也是同一个对象,自然是 AB。

想让 3 个位置是独立对象,应该用:

csharp
var arr = Enumerable.Range(0, 3)
                    .Select(_ => new System.Text.StringBuilder("A"))
                    .ToArray();

掌握了初始化和默认值的「出厂设置」,你就再也不怕被 NullReferenceException 偷袭了。下一篇我们将进入多维数组的世界,看看「Excel 表格」式的二维数组如何定义和使用。