关于 dotnet-csharp-nullable-reference-types
为 C# 项目提供可空引用类型(NRT)的标注与迁移指导,适用于新旧 .NET 代码库。内容包括渐进式启用、空值契约和常见误用,帮助减少空值相关问题。
主要功能
- 01渐进式启用 NRT
- 02标注可空与非可空类型
- 03使用属性表达空值契约
- 04识别空值抑制符误用
- 05说明 EF Core 空值映射
适用场景
- 01为旧项目分阶段启用 NRT
- 02改进 Try 模式方法的空值标注
- 03排查接口实现中的可空性不一致
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-nullable-reference-types安装后在 Claude Code 中输入/dotnet-csharp-nullable-reference-types确认已加载。
详细介绍
dotnet-csharp-nullable-reference-types
可空引用类型(NRT)注解策略、旧版代码库迁移指南,以及 AI Agent 最常犯的注解错误。所有现代 .NET 模板(net6.0+)默认启用 NRT,但许多现有代码库仍需迁移。
交叉引用:[skill:dotnet-csharp-coding-standards],了解 null 处理风格;[skill:dotnet-csharp-modern-patterns],了解涉及 null 的模式匹配。
快速参考:不同 TFM 的 NRT 默认设置
| TFM | <Nullable> 默认值 | 备注 |
|---|---|---|
| net8.0+ | enable(在模板中) | 新项目默认启用 NRT |
| net6.0/net7.0 | enable(在模板中) | 与 net8.0 相同 |
| netstandard2.0/2.1 | 未设置 | 必须显式启用 |
| net48 / 更早版本 | 未设置 | 必须显式启用 |
重要: TFM 不会强制启用 NRT——真正起作用的是 <Nullable>enable</Nullable> MSBuild 属性。升级到 net8.0 的旧版项目可能并未启用此属性。
启用 NRT
整个项目(推荐)
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
单个文件(迁移)
#nullable enable // top of file -- enables NRT for this file only
迁移策略
对于大型代码库,可逐步启用 NRT:
- 在项目中设置
<Nullable>enable</Nullable> - 在每个现有文件的顶部添加
#nullable disable(使用脚本或 IDE 工具) - 逐个文件移除
#nullable disable,并在过程中修复警告 - 跟踪进度:统计剩余的
#nullable disable指令
注解模式
可空与不可空
public class UserService
{
// Non-nullable: must never be null
private readonly IUserRepository _repo;
// Nullable: explicitly may be null
public User? FindByEmail(string email)
{
return _repo.FindByEmail(email); // may return null
}
// Non-nullable parameter: caller must provide non-null
public async Task<User> GetByIdAsync(int id, CancellationToken ct = default)
{
return await _repo.GetByIdAsync(id, ct)
?? throw new NotFoundException($"User {id} not found");
}
}
可空性属性
使用 System.Diagnostics.CodeAnalysis 中的属性来表达编译器无法推断的可空性约定:
using System.Diagnostics.CodeAnalysis;
// Output is non-null when method returns true
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
value = _dict.GetValueOrDefault(key);
return value is not null;
}
// Guarantees member is non-null after method returns
public class Connection
{
public string? ConnectionString { get; private set; }
[MemberNotNull(nameof(ConnectionString))]
public void Initialize(string connectionString)
{
ConnectionString = connectionString
?? throw new ArgumentNullException(nameof(connectionString));
}
}
// Return is non-null if input is non-null
[return: NotNullIfNotNull(nameof(input))]
public static string? Trim(string? input)
{
return input?.Trim();
}
// Parameter must not be null when method returns (for assertion methods)
public static void EnsureNotNull([NotNull] object? value, string paramName)
{
if (value is null)
{
throw new ArgumentNullException(paramName);
}
}
// Method never returns normally (always throws)
[DoesNotReturn]
public static void ThrowNotFound(string message)
{
throw new NotFoundException(message);
}
常用属性摘要
| 属性 | 位置 | 含义 |
|---|---|---|
[NotNullWhen(true)] | out 参数 | 方法返回 true 时不为 null |
[NotNullWhen(false)] | out 参数 | 方法返回 false 时不为 null |
[MemberNotNull] | 方法 | 调用后指定成员不为 null |
[MemberNotNullWhen(true)] | 方法 | 返回 true 时指定成员不为 null |
[NotNullIfNotNull] | 返回值 | 指定参数不为 null 时,返回值也不为 null |
[NotNull] | 参数 | 调用后参数不为 null(断言) |
[DoesNotReturn] | 方法 | 方法不会返回(总是抛出异常) |
[AllowNull] | 参数/属性 | 即使类型不可空,调用方也可以传入 null |
[DisallowNull] | 参数/属性 | 即使类型可空,调用方也不得传入 null |
[MaybeNull] | 返回值/out | 即使类型不可空,返回值也可能为 null |
[MaybeNullWhen(false)] | out 参数 | 方法返回 false 时可能为 null |
Agent 常见陷阱
以下是 AI Agent 生成 C# 代码时最常犯的 NRT 错误。
1. 使用 !(null 抑制运算符)消除警告
// WRONG -- hides real null bugs
var user = _repo.FindByEmail(email)!; // will throw NRE if null
string name = user!.Name!; // double suppression is a red flag
// CORRECT -- handle null explicitly
var user = _repo.FindByEmail(email)
?? throw new NotFoundException($"User with email {email} not found");
只有在你掌握了编译器无法验证的信息时才应使用 ! 运算符(例如,调试断言之后,或测试代码中使用已知数据时)。
2. 忽略可空性警告
// WRONG -- warning CS8602: Dereference of a possibly null reference
public string GetDisplayName(User? user)
{
return user.Name; // possible NRE!
}
// CORRECT
public string GetDisplayName(User? user)
{
return user?.Name ?? "Unknown";
}
3. 接口实现中的可空性错误
// Interface says nullable
public interface IRepository
{
User? FindById(int id);
}
// WRONG -- implementation changes contract
public class UserRepository : IRepository
{
public User FindById(int id) // removed nullable -- inconsistent
{
return _db.Users.First(u => u.Id == id);
}
}
// CORRECT -- preserve nullable contract
public class UserRepository : IRepository
{
public User? FindById(int id)
{
return _db.Users.FirstOrDefault(u => u.Id == id);
}
}
4. Try 模式方法缺少 [NotNullWhen]
// WRONG -- compiler doesn't know result is non-null on success
public bool TryParse(string input, out Order? result)
{
// ...
}
// After call: result is still Order? even when method returned true
// CORRECT
public bool TryParse(string input, [NotNullWhen(true)] out Order? result)
{
// ...
}
// After call: result is Order (non-nullable) when method returned true
5. 混淆可空值类型与可空引用类型
// These are different systems!
int? nullableInt = null; // Nullable<int> -- always existed
string? nullableStr = null; // NRT annotation -- compile-time only, no runtime type change
// typeof(int?) != typeof(int), but typeof(string?) == typeof(string)
可空性的泛型约束
// Constrain to non-nullable reference types
public class Repository<T> where T : class
{
public T Get(int id) => ...; // T is non-nullable
public T? Find(int id) => ...; // T? is nullable
}
// Allow both nullable and non-nullable
public class Cache<T> where T : notnull
{
public T GetOrDefault(string key, T defaultValue) => ...;
}
// Allow nullable type parameter (default)
public class Wrapper<T>
{
public T? Value { get; set; } // T? behavior depends on whether T is value or reference type
}
集合与可空性
// Dictionary: value might not exist
Dictionary<string, User> users = new();
if (users.TryGetValue(key, out var user))
{
// user is non-null here (with proper NRT annotations in BCL)
}
// Array/List of nullable items
List<string?> names = ["Alice", null, "Bob"];
foreach (var name in names)
{
if (name is not null)
{
Console.WriteLine(name.Length); // safe
}
}
// Non-nullable collection with nullable lookup
IReadOnlyList<Order> orders = GetOrders();
Order? first = orders.FirstOrDefault(); // FirstOrDefault returns T? for reference types
EF Core 与 NRT
EF Core 会根据 NRT 注解区分必需列和可选列:
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = ""; // NOT NULL column
public string? Notes { get; set; } // NULL column
public Address Address { get; set; } = null!; // Required navigation (EF convention)
}
注意: 对于 EF 保证会初始化的导航属性,= null! 可用于 EF Core。这是少数几种合理使用 null 抑制运算符的情况之一。
参考资料
评论
支持 Markdown