首页 / C# 入门教程 / 可空引用类型与可空值类型

C# 入门教程

可空引用类型与可空值类型

本教程共 100 篇 · 第 90 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

C#C# 入门教程编程语言可空类型Nullable

90. 可空引用类型与可空值类型

本节目标:学完能区分可空引用类型 string? 与可空值类型 int?,并安全处理可能为 null 的值。

null 带来的老问题

C# 早期里,引用类型(如 string)默认可为 null,但编译器不提醒你”这里可能是空”。运行时一访问 .Length,就抛出 NullReferenceException,俗称”空引用异常”。

值类型(如 int)默认不能为 null,有时却确实需要”没有值”的语义,比如数据库里的可空字段。

Note

可空性分两类:可空引用类型(string?,C# 8 引入)和可空值类型(int?,C# 2 就有)。二者机制不同,别混。

可空引用类型 string?

C# 8 起,引用类型默认”不可空”。写成 string? 才显式允许 null,编译器据此给你警告。

#nullable enable
string notNull = "hello";
// notNull = null;      // 警告:可能为 null
string? maybeNull = null; // 合法

#nullable enable 打开可空注解上下文,之后编译器才帮你做静态检查。

Tip

.NET 6+ 的新项目模板默认开启可空检查。读到 string? 就该警觉:用到它前先判空。

为什么引入可空引用类型

引用类型”默认可为 null”是 C# 从早期语言继承来的老设计。发明 null 的 Tony Hoare 后来称它为”十亿美元的错误”——太多系统在运行期因意外 null 而崩溃。

C# 8 的可空引用类型不是改运行时,而是给编译器加了一层”静态注解”:默认引用不可空,只有显式写 ? 才允许 null。它把”这里可能为 null”从潜规则变成写在类型上的契约,编译器据此在写代码时就报警。

Note

可空引用类型改变的是”你和编译器之间的约定”,运行期行为没变。它让你在编译阶段就接住大部分疏漏,而不是等上线后崩溃。

可空值类型 Nullable

值类型本不能为 null。加 ? 后变成 Nullable<T>,既能装值也能装”无值”。

int? age = null;     // 合法,表示未知
int normal = 20;     // 普通 int 不能赋 null
Console.WriteLine(age.HasValue ? age.Value : -1);

Nullable<T> 是结构体,带 HasValueValue 两个成员。可空值类型本质是”值 + 是否有值”的包裹。

?. 空条件访问

?. 在对象为 null 时直接短路返回 null,不再往下访问,避免异常。

string? name = null;
int? len = name?.Length; // name 为 null,len 得到 null,不抛异常
Console.WriteLine(len ?? 0);

相比先 if (name != null) 再取长度,?. 一行搞定,清爽很多。

Note

?. 返回的是可空类型:name?.Length 得到 int?。要当普通 int 用,配 ?? 给默认值。

?? 空合并运算符

?? 表示”左边不为 null 就用左边,否则用右边”。它常和 ?. 搭档,给个兜底值。

string? input = null;
string result = input ?? "默认值";
Console.WriteLine(result); // 默认值

int length = input?.Length ?? 0; // 取长度,空则 0

C# 8 还加了 ??=(空合并赋值):左边为 null 才赋值。

string? cfg = null;
cfg ??= "缺省配置";

与模式匹配配合

判空也能用模式:is not null 表达非空,is null 判空。还能在声明模式里处理可空。

object? obj = "text";
if (obj is string s)
{
    Console.WriteLine(s.Length); // 匹配成功才进,s 非空
}
Tip

处理可空值类型时,is int n 这种声明模式既判空又取出值,比 HasValue 更优雅。

旧版本对比

C# 8 之前没有可空引用类型。那时的 string 默认允许 null,全靠开发者自觉,编译器不报错。

// 旧写法(无 #nullable 上下文)
string name = null; // 不报错,但埋雷
// 新写法应写 string? name = null; 让意图显式
Warning

可空注解是编译期静态分析,不是运行期保险。它帮你提前发现疏漏,但不能替代运行时的 null 检查。

两类可空的本质区别

名字都带”可空”,机制却不同,用一张表说清:

维度可空引用类型 string?可空值类型 int?
引入版本C# 8C# 2
底层机制编译期注解,仍是普通引用Nullable<T> 结构体包裹
运行期是否真防 null否,只提醒是,值类型本身不能为 null
典型用途表示”可能没有对象”表示”可能没有数值”(如数据库字段)
Tip

记一句:引用可空是”编译器帮你盯”,值类型可空是”值真的能装无”。两者都写 ?,含义机制却不同,别当成一回事。

新手易踩的坑

坑一:以为 string? 运行时会阻止 null。不会,它只是编译期提醒,运行期传 null 仍可能出错。

坑二:?. 返回可空类型,忘了 ?? 兜底,把 int? 赋给 int 编译失败。

坑三:在没开 #nullable enable 的文件里写 string?,效果可能达不到预期,检查项目设置。

坑四:过度使用 ! 强制断言非空(name!),绕过检查等于埋雷,能免则免。

Tip

设计 API 时,明确哪些参数/返回值可为 null,用 ? 标出来,调用方一眼便知该不该判空。

Nullable 的结构与装箱

int? 本质是 Nullable<int> 结构体,内部两个字段:一个 bool HasValue 和一个 T value。它不是引用,不额外在堆上分配。

装箱(boxing,第 48 章详讲)时有个特别行为:若 HasValue 为 false,装箱结果是 null;为 true,则只装箱里面的 value

int? a = null;
object o1 = a;            // o1 是 null 引用
int? b = 5;
object o2 = b;            // o2 装箱的是 5,不是 Nullable<int>
Console.WriteLine(o1 == null); // True
Note

这点容易误解:可空值类型装箱后,null 就是真 null,非 null 就退化成底层值类型,没有”包裹”残留。

泛型里的可空

泛型约束要注意:T?where T : struct 时表示 Nullable<T>;在 where T : class 时表示”可空引用注解”,语义不同。

T? FirstOrNull<T>(List<T> list) where T : struct
    => list.Count > 0 ? list[0] : null; // 返回 Nullable<T>

写泛型方法时,可空含义随约束变化。新手若混用,类型推断会让你困惑。

Tip

不确定 T? 在当前约束下代表啥,看 T 是值类型还是引用类型约束,两者机制完全不同。

迁移旧代码的策略

老项目默认没开可空检查,加 #nullable enable 后可能冒出一片警告。别慌,逐文件开启更稳:

  1. 先给公共 API 的签名标 ?,表达真实意图。
  2. ?.?? 处理已有可能为 null 的路径。
  3. 对确实非空但编译器不信的,用 ! 谨慎断言(尽量少用)。
#nullable enable
string? ReadConfig() => null;
string Get() => ReadConfig() ?? "默认"; // 用 ?? 兜底,消除警告
Warning

! 强制消警告是”我保证非空”,不是”编译器保证”。乱用会掩盖真实 null 风险,能判空就别用 !

可空与集合、字典

集合元素也可空:List<string?> 表示列表里允许有 null 项;Dictionary<int, string?> 表示值可空。

List<string?> names = ["甲", null, "乙"];
foreach (var n in names)
    Console.WriteLine(n?.Length ?? 0); // null 项安全处理

Dictionary<int, string?> map = new() { [1] = null };
string? v = map[1];
Console.WriteLine(v ?? "无值");
Note

DictionaryTryGetValue 配合可空也常见:取不到键时给默认,比直接 [] 访问更安全。

警告等级与注解传播

可空上下文有两种:注解上下文(是否允许 ? 语法)与警告上下文(是否对可能的 null 报警)。项目级 #nullable enable 同时打开两者。

方法参数标了 string?,调用方传普通 string 不会警告;但反过来传 string? 给要求非空的参数,编译器就会提醒你先判空。

void Print(string s) => Console.WriteLine(s.Length);
string? maybe = "x";
// Print(maybe); // 警告:maybe 可能为 null
Print(maybe!);   // 强制,但慎用
Tip

让可空注解沿方法签名”流动”:该可为空的地方显式 ?,编译器就能全程帮你盯紧 null。

一道常见面试题直觉

“可空引用类型能完全杜绝 NullReferenceException 吗?“答案是不能——它是编译期分析,不是运行期保险。

它把大量”可能为 null 却没判”的疏漏在写代码时就标红,大幅提升安全性,但:

  • 反射、反序列化等绕过编译检查的路径仍可能注入 null;
  • 用了 ! 强制断言就绕过了保护。

所以运行期必要的 null 检查仍不可省,尤其在处理外部输入时。

Warning

别因为开了可空就觉得”天下无 null”。它是强大的提醒工具,不是运行时防护盾,外部数据照样要校验。

小结

string? 是编译期可空注解,提醒你”可能为 null”;int?Nullable<T> 包裹,让值类型也能表示”无值”。?. 安全访问、?? 给默认值,二者配合基本消灭空引用异常。可空是提醒而非保险,关键处仍要判空。至此,本教程的异步与模式匹配核心已铺开,后续章将转向内存与文件等高级主题。