如何在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
相关产品推荐
相关产品推荐

