跳到主要内容

2.3 可空引用类型

可空引用类型让编译器帮助你区分“这个引用应该有值”和“这个引用允许为空”。它不能消灭所有空引用异常,但能把大量问题提前到编译阶段暴露出来。

学习目标

  • 理解 stringstring? 的语义差异。
  • 能根据业务规则设计允许为空和不允许为空的属性。
  • 能读懂并处理编译器的空值警告。
  • 能在 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 不应该为空,所以使用 stringrequiredDescription 是业务上的可选字段,所以使用 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 可空属性与数据库列约束。