C# 数组的初始化和默认值:新柜子的「出厂设置」
引言:新柜子里到底装了什么
你去超市租了一排储物柜,还没放任何东西。这时候打开柜子,里面是什么?
- 是「什么都没有」吗?
- 还是有一团空气?
- 或者贴着一张「空」的纸条?
在 C# 的世界里,这个问题有明确答案:数组一旦创建,每个位置都会被系统自动填入一个「出厂设置」——默认值。不同类型的柜子,出厂设置不同:
- 装整数的柜子,出厂放的是
0 - 装小数的柜子,出厂放的是
0.0 - 装布尔的柜子,出厂放的是
false - 装字符串的柜子,出厂放的是
null(真空,连空气都没有)
理解「初始化方式」和「默认值机制」,是避免程序崩溃(尤其是臭名昭著的 NullReferenceException)的关键。本文带你彻底搞懂这两个话题。
一、入门引导:数组基础与两大阵营
1.1 数组基础(30 秒回顾)
数组 = 一排固定长度、类型统一、内存连续的储物柜,通过从 0 开始的索引访问。
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),直接用会崩溃
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); // 崩溃!NullReferenceException1.3 底层原理极简版:默认值从哪来
当你写下 new int[5] 时,.NET 运行时会做两件事:
- 在堆(Heap)内存中开辟一块连续空间,大小 = 5 × 4 字节(一个 int 占 4 字节)
- 把这块内存全部清零(所有二进制位设为 0)
这就是默认值的来源——内存清零。对值类型来说,全 0 恰好就是 0、0.0、false;对引用类型来说,全 0 表示「空地址」,也就是 null。
所以默认值不是编译器「好心送你」的,而是内存分配时的自然结果。这也解释了为什么默认值是自动的、零开销的——清零内存本来就是分配内存的必要步骤。
二、数组初始化方式大全
2.1 方式一:指定长度的基础初始化
语法:数据类型[] 数组名 = new 数据类型[长度];
是什么:只告诉编译器「要几个柜子」,不放东西,每个位置自动填默认值。
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 数据类型[] { ... })
是什么:建柜子的同时把东西放好,长度由元素个数自动决定。
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]); // 输出:李四
}
}什么时候用:
- ✅ 写代码时就知道所有元素(配置项、固定选项、测试数据)
常见坑点:
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, ... };
是什么:让编译器根据元素自动推断数组类型。
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 表格(行 × 列),三维数组像一摞表格。注意和后面的「交错数组」区分。
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 方式五:交错数组(锯齿数组)的初始化
是什么:「数组的数组」——外层数组的每个元素本身就是一个数组,且每个内层数组长度可以不同(像锯齿一样参差不齐)。
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 交错,一图看懂:
二维数组 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 配合方法返回数组,实现更灵活的初始化。
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 月发布)引入了集合表达式,用 [...] 统一初始化各种集合:
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 引入的 ^(从末尾数索引)和 ..(范围)运算符,常用于从已有数组切片创建新数组:
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 才初始化」的懒加载模式:
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 对应的值。
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 / byte | 0 | 整数家族全是 0 |
float / double / decimal | 0.0 | 浮点家族全是 0.0 |
bool | false | |
char | '\0'(空字符) | Unicode 码为 0 的字符 |
DateTime | 0001/1/1 00:00:00 | DateTime.MinValue |
| 枚举 | 值为 0 的成员 | 若枚举没有 0 值成员,则为数值 0(可能无对应名称!) |
枚举默认值的坑:
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)数组的默认值
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。
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 调用任何成员(方法、属性、索引器),程序立刻崩溃:
string[] names = new string[3];
// 三种典型死法:
// names[0].Length; // NullReferenceException
// names[0].ToUpper(); // NullReferenceException
// if (names[0].StartsWith("张")) // NullReferenceException防御性写法:
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 的值类型」,语法是在值类型后加 ?。
核心差异:
| 类型 | 默认值 | 能否表示「没有值」 |
|---|---|---|
int | 0 | ❌(0 就是一个普通的数) |
int? | null | ✅(null 明确表示「没有数据」) |
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。实现方法:
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 误区一:以为引用类型数组「自动有元素」
错误代码:
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 个柜子,但柜子里连张纸条都没有,你却想往「柜子里的学生档案」上写名字。
正确代码:
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 误区二:交错数组忘记初始化内层
错误代码:
int[][] jagged = new int[2][];
jagged[0][0] = 100; // 崩溃!jagged[0] 是 null,null 不能按下标访问原因分析:new int[2][] 只建了外层数组,jagged[0] 和 jagged[1] 都是 null。
正确代码:
int[][] jagged = new int[2][];
jagged[0] = new int[3]; // 先给内层数组「安柜子」
jagged[0][0] = 100; // 再放东西4.3 误区三:混淆多维数组和交错数组的语法
错误代码:
int[,] grid = new int[2][]; // 编译错误![,] 是二维数组,后面不能用 [][]
int[][] jagged = new int[2,3]; // 编译错误![][] 是交错数组,不能用 [,] 的语法
int[,] data = { 1, 2, 3 }; // 编译错误!二维数组必须用嵌套大括号正确代码:
// 二维数组:一对括号里用逗号
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 误区四:用「重复引用」填充对象数组
错误代码:
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 对象。改任意一个,其他位置看到的也跟着变——又浅拷贝陷阱。
正确代码:
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 误区五:用默认值判断「有没有数据」
错误代码:
// 想统计「录入成绩的学生人数」,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 又可能是真实数据,两者无法区分。
正确代码:用可空类型明确表示「未录入」:
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 误区六:切片范围越界
错误代码:
int[] numbers = { 1, 2, 3 };
// int[] slice = numbers[0..5]; // 运行时异常!数组只有 3 个元素
// Console.WriteLine(numbers[^4]); // 运行时异常!倒数第 4 个不存在原因分析:C# 8 的 ^ 和 .. 并不豁免越界检查,范围超出数组长度照样抛 IndexOutOfRangeException。
正确代码:
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 天的营业状态,默认全部「营业中」,周末单独设置。
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}");
}
}
}输出:
周一:营业中
周二:营业中
周三:营业中
周四:营业中
周五:营业中
周六:休息
周日:休息最佳实践:
- 枚举显式定义
0值成员,让数组默认值有明确业务含义 Array.Fill(arr, value, startIndex, count)可以只填充指定范围,比循环更简洁
5.2 场景二:接口参数数组的防御性处理
需求:编写一个接收学生名单的方法,要妥善处理各种「不靠谱」的传入。
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 元素
}
}输出:
名单为空
名单为空
学生 1:张三
学生 2:(未命名)
学生 3:李四
有效名单:2 人最佳实践:
- 公共方法的数组参数,永远先判
null再判Length - 遍历引用类型数组时,对元素做
??兜底,防止个别null炸掉整个循环 - 参数标注
string[]?(C# 8 可空引用类型),明确告诉调用方「这里可以传 null」
5.3 场景三:缓存数组的懒初始化
需求:一个开销很大的数据数组,只在第一次使用时才创建,之后复用。
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
}
}输出:
服务已创建,缓存还没加载
(正在从数据库加载价格数据...)
拿到 4 条价格
又拿到 4 条价格
缓存第一条仍是:19.9最佳实践:
- 用
??=实现懒加载,避免程序启动时就创建大数组 - 对外返回
Clone()副本而不是缓存本身——否则调用方一个Sort就把你的缓存污染了 - 缓存字段用可空标注
double[]?,语义清晰
六、知识点速查表
6.1 初始化语法速查
// ==== 一维数组 ====
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 | ✅ 直接用 |
bool | false | ✅ 直接用 |
char | '\0' | ✅ 直接用 |
DateTime | 0001/1/1 | ✅ 直接用 |
| 枚举 | 数值 0 的成员 | ⚠️ 建议定义 None = 0 |
string / 自定义类 | null | ❌ 必须先判空或赋值 |
| 结构体 | 成员各自默认 | ⚠️ 结构体内的引用成员是 null |
可空值类型 int? | null | ⚠️ 用 .HasValue 判断 |
| 交错数组内层 | null | ❌ 必须先 new 内层 |
6.3 避坑清单
- 引用类型数组创建后全是 null——用之前先判空(
?.、??)或逐个 new - 交错数组要先 new 内层——
new int[3][]后内层全是 null [,]和[][]别搞混——逗号在括号里是多维,括号叠括号是交错Array.Fill/Repeat填对象是浅拷贝——需要独立对象用循环或Select(_ => new ...)- 默认值 0 无法区分「没数据」——用
int?可空类型 - 切片不豁免越界——
arr[0..n]的 n 不能超过长度 - 对外暴露数组先 Clone——防止调用方污染你的内部数据
七、入门练习题
基础初始化:分别用三种方式创建一个包含 5 个科目名称的
string数组(指定长度后逐个赋值、声明即赋值、隐式类型)。默认值判断:创建
int[]、bool[]、string[]、DateTime[]各一个(长度 3),不写任何赋值语句,直接输出每个数组的第一个元素,验证默认值。多维初始化:创建一个 3 行 4 列的二维数组,用嵌套循环填入「行号 × 列号」的值,然后按表格形式打印出来。
交错数组:创建一个交错数组表示 3 个小组的成员名单(第 1 组 3 人、第 2 组 5 人、第 3 组 2 人),打印每个小组的人数和成员。
可空类型:用
int?[]表示 5 名学生的成绩,其中 2 人未录入(保持 null),统计已录入人数和已录入成绩的平均分(提示:用Where(s => s.HasValue))。切片练习(C# 8+):对数组
{10, 20, 30, 40, 50, 60}分别取出:前 3 个、后 2 个、中间第 2~4 个,并验证原数组未被修改。思考题:下面代码的输出是什么?为什么?
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 个位置是独立对象,应该用:
var arr = Enumerable.Range(0, 3)
.Select(_ => new System.Text.StringBuilder("A"))
.ToArray();掌握了初始化和默认值的「出厂设置」,你就再也不怕被 NullReferenceException 偷袭了。下一篇我们将进入多维数组的世界,看看「Excel 表格」式的二维数组如何定义和使用。