C# 可空引用类型:把空值风险提前暴露
介绍 nullable reference types 的使用方式、常见警告和 API 设计思路,让 null 从运行时异常变成编译期提示。
NullReferenceException 是很多 C# 项目里最常见也最无聊的错误之一。可空引用类型的价值,就是把“这里可能是 null”这件事提前放到编译期讨论。
开启可空分析
在项目文件中启用:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
开启后,引用类型默认被视为不可为 null:
string name = null; // 编译器警告
如果某个值确实可能为空,需要显式写成 string?。
string? nickname = FindNickname(userId);
这个问号不是运行时特性,而是给编译器和读代码的人看的约定:这里必须处理空值。
T 和 T? 是 API 语义
方法签名里是否带 ?,就是调用方和实现方之间的契约。
public User? FindUser(Guid id)
{
return users.TryGetValue(id, out var user) ? user : null;
}
这个签名告诉调用方:用户可能不存在,请处理。
var user = FindUser(id);
if (user is null)
{
return NotFound();
}
return Ok(user.Name);
反过来,如果一个方法返回 User,就应该尽力保证它不会返回 null。否则类型签名就在撒谎。
不要滥用 !
空值宽恕操作符 ! 可以告诉编译器“我确定这里不是 null”。
var name = user!.Name;
它不会改变运行时行为。如果 user 实际上是 null,程序仍然会抛异常。
! 适合用于编译器无法理解、但你能证明安全的场景,例如测试初始化、框架注入或特定验证流程之后。它不适合用来压掉所有警告。
构造函数和必填属性
启用 nullable 后,类的非空属性需要在构造时被初始化。
public sealed class Article
{
public Article(string title)
{
Title = title;
}
public string Title { get; }
}
如果对象需要通过对象初始化器创建,可以使用 required 表达“调用方必须赋值”。
public sealed class Article
{
public required string Title { get; init; }
public string? Summary { get; init; }
}
这样比把所有属性都写成 string? 更准确,因为它区分了“必填但稍后初始化”和“真的可以为空”。
集合也要表达空值
下面两个类型含义不同:
List<string>? names
List<string?> names
第一个表示整个列表可能为空;第二个表示列表存在,但里面的元素可能为空。写 API 时要尽量表达清楚。
小结
可空引用类型不是为了让代码多几个问号,而是让空值成为设计的一部分。
一个实用习惯是:
- 能不返回 null,就不要返回 null。
- 可能不存在的结果,用
T?明确表达。 - 输入参数如果不允许 null,就保持非空类型。
- 少用
!,多用判断和清晰的初始化。
当类型签名讲真话,调用方就不需要靠猜。