c# - 在 C# 8.0 中反序列化对象时处理临时空引用类型?
问题描述
我正在尝试 C# 8.0,并且我想为整个项目启用空引用检查。我希望我可以改进我的代码设计,并且不会在任何代码范围内禁用可空性上下文。
我在反序列化对象图时遇到了问题。对象之间存在引用,但对于最终用户视图,对象图中的所有引用都必须有一个值。
换句话说,在反序列化过程中,引用可能是null
,但是在所有对象完成加载后,最终过程会将所有对象链接在一起,从而解决这些null
引用。
解决方法
我已经能够使用几种不同的技术来解决这个问题,并且它们都按预期工作。然而,它们还通过引入大量额外的脚手架大大扩展了代码。
选项#1:影子类
例如,我尝试为每种对象编写一个配对类,在反序列化期间将它们用作中间对象。在这些配对类中,所有引用都允许为null
. 反序列化完成后,我从这些类中复制所有字段并将它们转换为真实对象。当然,使用这种方法,我需要编写很多额外的代码。
选项#2:影子成员
或者,我尝试放置一个可为空的字段和一个不可为空的属性。这类似于以前的方法,但我使用的是配对成员而不是配对类。然后我为每个字段添加一个内部设置器。这种方法的代码比第一种方法少,但它仍然大大增加了我的代码库。
选项#3:反思
传统上,如果不考虑性能,我会使用反射来管理反序列化,这样每个类几乎没有额外的代码。但是编写自己的解析代码有一些好处——例如,我可以输出更多有用的错误消息,包括调用者如何解决问题的提示。
但是当我引入可空字段时,我的解析代码会大大增加——而且唯一的目的是满足代码分析。
示例代码
为了演示,我尽量简化了代码;我的实际课程显然不止于此。
class Person
{
private IReadOnlyList<Person>? friends;
internal Person(string name)
{
this.Name = name;
}
public string Name { get; }
public IReadOnlyList<Person> Friends => this.friends!;
internal SetFriends(IReadOnlyList<Person> friends)
{
this.friends = friends;
}
}
class PersonForSerialize
{
public string? Name { get; set; }
public IReadOnlyList<string> Friends { get; set; }
}
IReadOnlyList<Person> LoadPeople(string path)
{
PersonForSerialize[] peopleTemp = LoadFromFile(path);
Person[] people = new Person[peopleTemp.Count];
for (int i = 0; i < peopleTemp.Count; ++i)
{
people[i] = new Person(peopleTemp[i].Name);
}
for (int i = 0; i < peopleTemp.Count; ++i)
{
Person[] friends = new Person[peopleTemp[i].Friends.Count];
for (int j = 0; j < friends.Count; ++j)
{
string friendName = peopleTemp[i].Friends[j];
friends[j] = FindPerson(people, friendName);
}
people[i].SetFriends(friends);
}
}
问题
有没有一种方法可以满足 C# 8.0 中对反序列化期间仅临时为空的属性的空引用检查,而无需为每个类引入大量额外代码?
解决方案
您担心虽然您的对象不打算有null
成员,但这些成员将不可避免地存在null
于您的对象图的构建过程中。
归根结底,这是一个非常普遍的问题。是的,它影响反序列化,但也影响对象的创建,例如,在映射或数据绑定期间,例如数据传输对象或视图模型。通常,这些成员在构造对象和设置其属性之间的很短的时间内为空。其他时候,它们可能会在更长的时间内处于不确定状态,因为您的代码例如完全填充依赖关系数据集,正如您的互连对象图所要求的那样。
幸运的是,微软已经解决了这个确切的场景,为我们提供了两种不同的方法。
选项 #1:Null-Forgiving 运算符
第一种方法,正如@andrew-hanlon 在他的回答中指出的那样,是使用null-forgiving operator。然而,可能不是立即显而易见的是,您可以直接在不可为空的成员上使用它,从而完全消除您的中间类(例如,PersonForSerialize
在您的示例中)。事实上,根据您的确切业务需求,您可能可以将您的Person
课程简化为以下简单的内容:
class Person
{
internal Person() {}
public string Name { get; internal set; } = null!;
public IReadOnlyList<Person> Friends { get; internal set; } = null!;
}
选项 #2:可为空的属性
更新:从 2021 年 3 月 9 日发布的 .NET 5.0.4 (SDK 5.0.201) 开始,以下方法现在会产生
CS8616
警告。鉴于此,您最好使用上面概述的 null-forgiving 运算符。
第二种方法为您提供了相同的确切结果,但这样做是通过可空属性为 Roslyn 的静态流分析提供提示。这些需要比 null-forgiving 运算符更多的注释,但也更明确地说明正在发生的事情。事实上,我实际上更喜欢这种方法,因为它对于不习惯语法的开发人员来说更加明显和直观。
class Person
{
internal Person() {}
[NotNull, DisallowNull]
public string? Name { get; internal set; };
[NotNull, DisallowNull]
public IReadOnlyList<Person>? Friends { get; internal set; };
}
在这种情况下,您可以通过将可null
空性指示符 ( ?
) 添加到返回类型(例如,IReadOnlyList<Person>?
)来明确承认成员。但是您随后使用 nullable 属性告诉消费者,即使成员被标记为 nullable:
[NotNull]
:可以为空的返回值永远不会为空。[DisallowNull]
:输入参数永远不应为空。
分析
无论您使用哪种方法,最终结果都是相同的。如果在不可为空的属性上没有容错运算符,您的成员将收到以下警告:
CS8618
:不可为空的属性“名称”未初始化。考虑将属性声明为可为空。
或者,如果不对[NotNull]
可空属性使用该属性,则在尝试将其值分配给不可为空的变量时会收到以下警告:
CS8600
: 将 null 文字或可能的 null 值转换为不可为 null 的类型。
或者,类似地,在尝试调用值的成员时:
CS8602
:取消引用可能为空的引用。
然而,使用这两种方法中的一种,您可以使用默认 ( null
) 值构造对象,同时仍然让下游消费者相信这些值实际上不会是null
- 并且因此允许他们使用这些值而无需警卫条款或其他防御性代码。
相反,当使用这些方法中的任何一种时,在尝试为这些成员分配值时仍然会收到以下警告:null
CS8625
: 无法将 null 文字转换为不可为 null 的引用类型。
没错:您甚至会在分配给string?
属性时得到它,因为这[DisallowNull]
是指示编译器执行的操作。
结论
采用哪种方法取决于您。因为它们都产生相同的结果,所以这纯粹是一种风格偏好。无论哪种方式,您都可以null
在构造过程中保留成员,同时仍然可以实现 C# 的不可为空类型的好处。
推荐阅读
- html - Html Form 发送字符串而不是数组
- qt - 在 QT 中,为什么父母可以使用子信号?
- json - 角度显示嵌套值,同时忽略动态键
- visual-studio - 您如何在 VS 2017 中将 .hpp 扩展名设为默认
- javascript - Ionic 3 将 facebook 用户保存到 firestore 数据库
- python - 如何修复 code_to_names
- mysql - MySQL - 需要找到 SUM 查询的 MAX
- c# - 如何将页面加载到从另一个 wpf 页面开始的窗口中的框架中?
- macos - Unix 帮助将第 2 行和第 3 行移动到第 1 行的中间
- java - 擦除:以下函数是否会覆盖 android 上的内存?