mcpField
D
dotnet-csharp-nullable-reference-types

dotnet-csharp-nullable-reference-types

github.com/wshaddix开发8677 次安装35 周前更新
代码仓库

关于 dotnet-csharp-nullable-reference-types

为 C# 项目提供可空引用类型(NRT)的标注与迁移指导,适用于新旧 .NET 代码库。内容包括渐进式启用、空值契约和常见误用,帮助减少空值相关问题。

主要功能

  1. 01渐进式启用 NRT
  2. 02标注可空与非可空类型
  3. 03使用属性表达空值契约
  4. 04识别空值抑制符误用
  5. 05说明 EF Core 空值映射

适用场景

  1. 01为旧项目分阶段启用 NRT
  2. 02改进 Try 模式方法的空值标注
  3. 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.0enable(在模板中)与 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:

  1. 在项目中设置 <Nullable>enable</Nullable>
  2. 在每个现有文件的顶部添加 #nullable disable(使用脚本或 IDE 工具)
  3. 逐个文件移除 #nullable disable,并在过程中修复警告
  4. 跟踪进度:统计剩余的 #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