You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C# 8.0中处理反序列化时的临时空引用类型(少新增代码)

处理C# 8.0空引用检查下的反序列化临时空引用问题

我完全理解你的痛点——启用全项目空引用检查后,处理反序列化时的临时空引用确实容易陷入“写大量冗余代码”的困境。好在C#提供了几个轻量级的方案,不需要影子类/成员或者复杂反射,就能满足要求:

方案1:用AllowNull和NotNull属性标注临时可空成员

通过System.Diagnostics.CodeAnalysis中的属性,你可以明确告诉编译器:某个字段在初始化阶段允许为null,但对外暴露的属性永远不会为null。这样既符合空引用检查规则,又不用额外写配对类。

修改你的Person类如下:

using System.Diagnostics.CodeAnalysis;

class Person { 
    [AllowNull] // 允许反序列化/初始化阶段设为null
    private IReadOnlyList<Person>? friends; 
    
    internal Person(string name) { this.Name = name; } 
    public string Name { get; } 
    
    [NotNull] // 对外保证该属性永远不为null(初始化完成后)
    public IReadOnlyList<Person> Friends => friends!; 
    
    internal void SetFriends(IReadOnlyList<Person> friends) { this.friends = friends; } 
}

这个方案只需要给字段和属性加两个属性标记,代码增量极小,同时保留了你原有的初始化逻辑。

方案2:用MemberNotNull强化方法的空状态契约

如果想让编译器更智能地识别“调用某个方法后字段不再为null”,可以给SetFriends方法加上MemberNotNull属性。这样编译器会知道,调用这个方法后,friends字段肯定有值,后续访问Friends属性时不会再提示空引用警告。

修改后的代码:

using System.Diagnostics.CodeAnalysis;

class Person { 
    private IReadOnlyList<Person>? friends; 
    
    internal Person(string name) { this.Name = name; } 
    public string Name { get; } 
    
    public IReadOnlyList<Person> Friends => friends!; 
    
    [MemberNotNull(nameof(friends))] // 告诉编译器:调用此方法后friends不为null
    internal void SetFriends(IReadOnlyList<Person> friends) { 
        this.friends = friends; 
    } 
}

这个方案进一步优化了代码分析体验,让编译器能准确跟踪字段的空状态,减少不必要的空断言。

方案3:结合序列化特性直接在原类处理引用解析

如果使用的是System.Text.Json或Newtonsoft.Json这类主流序列化库,可以利用私有构造函数和临时属性来直接处理引用解析,完全省去影子类的编写。

以System.Text.Json为例:

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

class Person { 
    private IReadOnlyList<Person>? friends; 
    
    public string Name { get; } 
    public IReadOnlyList<Person> Friends => friends!; 
    
    // 供序列化器调用的私有构造函数
    [JsonConstructor]
    private Person(string? name, IReadOnlyList<string>? friendNames) { 
        // 最终视图中Name必须有值,所以用!断言
        Name = name!; 
        // 确保friendNames不为null,避免后续空引用
        FriendNames = friendNames ?? System.Array.Empty<string>(); 
    }
    
    // 临时存储朋友名称的属性,仅用于反序列化
    [JsonPropertyName("friends")]
    public IReadOnlyList<string> FriendNames { get; private set; }
    
    [MemberNotNull(nameof(friends))]
    internal void ResolveFriends(IReadOnlyDictionary<string, Person> personLookup) { 
        friends = FriendNames.Select(name => personLookup[name]).ToList().AsReadOnly(); 
    }
}

对应的加载代码可以简化成:

IReadOnlyList<Person> LoadPeople(string path) { 
    var people = JsonSerializer.Deserialize<Person[]>(File.ReadAllText(path))!; 
    var personLookup = people.ToDictionary(p => p.Name); 
    foreach (var person in people) { 
        person.ResolveFriends(personLookup); 
    }
    return people;
}

这个方案把反序列化的临时状态封装在原类内部,既保持了代码的内聚性,又满足空引用检查的要求,代码增量远小于影子类方案。

方案对比

  • 对比你的选项1(影子类):上述方案都不需要编写额外的配对类,代码量减少80%以上
  • 对比你的选项2(影子成员):仅需添加少量属性标记或临时成员,不会显著增加代码复杂度
  • 对比你的选项3(反射):完全不需要依赖反射,性能更好,代码可读性更高

所有方案都严格遵守了“不关闭任何代码范围可空性上下文”的要求,同时保证了最终用户视图中所有引用都有值。

内容的提问来源于stack exchange,提问作者watson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:19:49