2.3 可空引用类型
可空引用类型让编译器帮助你区分“这个引用应该有值”和“这个引用允许为空”。它不能消灭所有空引用异常,但能把大量问题提前到编译阶段暴露出来。
学习目标
- 理解
string与string?的语义差异。 - 能根据业务规则设计允许为空和不允许为空的属性。
- 能读懂并处理编译器的空值警告。
- 能在 DTO、实体和服务方法中使用明确的空值契约。
应用场景
- API 请求中有可选字段,例如任务备注
Description。 - 数据库字段允许为空,例如用户头像地址
AvatarUrl。 - 查询结果可能不存在,例如按 ID 查找任务。
- 外部系统返回的数据质量不稳定,需要显式防御。
核心概念
| 写法 | 语义 | 示例 |
|---|---|---|
string | 期望永远不为空 | 任务标题、用户邮箱 |
string? | 允许为空,使用前要判断 | 备注、头像、可选手机号 |
! | 告诉编译器“我确认不为空” | 只在框架赋值或测试构造时谨慎使用 |
案例:任务创建请求
业务要求:创建任务时必须提供标题,备注可以不填。
public sealed record CreateTodoRequest(
string Title,
string? Description);
public sealed class TodoItem
{
public int Id { get; init; }
public required string Title { get; init; }
public string? Description { get; init; }
public bool IsCompleted { get; private set; }
public void Complete()
{
IsCompleted = true;
}
}
Title 不应该为空,所以使用 string 和 required。Description 是业务上的可选字段,所以使用 string?。
示例:查询结果为空
public interface TodoRepository
{
Task<TodoItem?> FindByIdAsync(int id, CancellationToken cancellationToken);
}
app.MapGet("/todos/{id:int}", async (
int id,
TodoRepository repository,
CancellationToken cancellationToken) =>
{
var todo = await repository.FindByIdAsync(id, cancellationToken);
return todo is null
? Results.NotFound(new {message = "任务不存在"})
: Results.Ok(todo);
});
返回 TodoItem? 明确表达“可能查不到”。调用方必须处理 null 分支,否则编译器会提醒。
重点难点
string?不是运行时新类型,它是编译期分析语义。!不是空值检查,只是压制警告;滥用会把风险藏回运行时。- 数据库允许为空、DTO 允许为空、领域模型允许为空是三件事,需要分别判断。
- 外部输入即使类型标成非空,也仍然需要校验空字符串和空白字符串。
常见误区
| 误区 | 推荐做法 |
|---|---|
所有属性都写成 string? 以避免警告 | 根据业务规则区分必填和可选 |
用 ! 快速消除所有警告 | 先补判断、默认值或构造函数约束 |
只判断 null,忽略空字符串 | 对用户输入使用 string.IsNullOrWhiteSpace |
练习
- 给
TodoItem增加可选截止日期DueDate,并在返回 DTO 中展示。 - 修改创建接口:标题为空白时返回 400。
- 写一个方法
GetDisplayDescription,没有备注时返回“暂无备注”。
延伸阅读
- C# 可空引用类型官方文档。
- ASP.NET Core 模型验证。
- EF Core 可空属性与数据库列约束。