使用反序列化的C#8.0可空引用类型时的最佳实践?

使用反序列化的C#8.0可空引用类型时的最佳实践?,c#,.net-core,c#-8.0,nullable-reference-types,C#,.net Core,C# 8.0,Nullable Reference Types,我正在尝试C#8.0,我想为整个项目启用空引用检查。我希望我可以改进我的代码设计,并且不禁用任何代码范围中的可空性上下文 我在反序列化对象图时遇到了一个问题。对象之间有引用,但对于最终用户视图,对象图中的所有引用都必须有一个值 换句话说,在反序列化过程中,引用可以是null,但在所有对象完成加载后,最后一个过程将所有对象链接在一起,从而解析null引用 我已经能够使用两种不同的技术来解决这个问题,它们都按照预期工作。然而,它们也大大扩展了代码,而且它们看起来并不特别优雅 例如,我尝试为每种对象编

我正在尝试C#8.0,我想为整个项目启用空引用检查。我希望我可以改进我的代码设计,并且不禁用任何代码范围中的可空性上下文

我在反序列化对象图时遇到了一个问题。对象之间有引用,但对于最终用户视图,对象图中的所有引用都必须有一个值

换句话说,在反序列化过程中,引用可以是
null
,但在所有对象完成加载后,最后一个过程将所有对象链接在一起,从而解析
null
引用

我已经能够使用两种不同的技术来解决这个问题,它们都按照预期工作。然而,它们也大大扩展了代码,而且它们看起来并不特别优雅

例如,我尝试为每种对象编写一个成对的类,在反序列化过程中将它们用作中间对象。在这些成对的类中,所有引用都允许为
null
。反序列化完成后,我复制这些类中的所有字段,并将它们转换为真实对象。当然,使用这种方法,我需要编写很多额外的代码

或者,我尝试放置一个可为null的字段和一个不可为null的属性。这与前面的方法类似,但我使用成对的成员而不是成对的类。然后我为每个字段添加一个内部setter。这种方法的代码比第一种方法少,但它仍然大大增加了我的代码库

传统上,在不考虑性能的情况下,我会使用反射来管理反序列化,这样每个类几乎没有额外的代码。但是编写自己的解析代码有一些好处,例如,我可以输出更有用的错误消息,包括关于调用方如何解决问题的提示

但是当我引入可为空的字段时,我的解析代码会大大增加。我想知道这是否是一个好的和适当的方法来做这件事


为了演示,我尽量简化了代码;我的实际课程显然远不止这些。我想知道是否有更好的代码风格

班级人员
{
私人IReadOnlyList?朋友;
内部人员(字符串名称)
{
this.Name=Name;
}
公共字符串名称{get;}
public-IReadOnlyList-Friends=>this.Friends!;
内部SetFriends(IReadOnlyList friends)
{
这个。朋友=朋友;
}
}
类PersonForSerialize
{
公共字符串?名称{get;set;}
公共IReadOnlyList好友{get;set;}
}
IReadOnlyList LoadPeople(字符串路径)
{
PersonForSerialize[]peopleTemp=LoadFromFile(路径);
人员[]人员=新人员[peopleTemp.Count];
对于(int i=0;i
由于C#8中不可为空的默认值,这一点当然会突出显示,但这确实是一个常见的“循环依赖”问题,在相互依赖的构造中一直存在

正如您所发现的,一个标准解决方案是使用受限setter。对于C#8,您可能希望使用“null宽恕”
null在对象图反序列化期间-允许您区分未构造集和有效空集,并最小化分配

例如:

class Person
{
    internal Person(string name, IReadOnlyList<Person> friends)
    {
        Name = name; Friends = friends
    }

    public string Name { get; }
    public IReadOnlyList<Person> Friends {get; internal set;}
}

class SerializedPerson { ... }

IEnumerable<Person> LoadPeople(string path)
{
    var serializedPeople = LoadFromFile(path);

    // Note the use of null!
    var people = serializedPeople.Select(p => new Person(p.Name, null!));

    foreach(var person in people)
    {
        person.Friends = GetFriends(person, people, serializedPeople);
    }

    return people;
}
班级人员
{
内部人员(字符串名称、IReadOnlyList好友)
{
姓名=姓名;朋友=朋友
}
公共字符串名称{get;}
公共IReadOnlyList好友{get;内部集合;}
}
类序列化Person{…}
IEnumerable LoadPeople(字符串路径)
{
var serializedPeople=LoadFromFile(路径);
//注意null的用法!
var people=serializedPeople.Select(p=>newperson(p.Name,null!));
foreach(人与人之间的变量)
{
person.Friends=GetFriends(person,people,serializedPeople);
}
还人,;
}

您担心的是,虽然对象不打算包含
null
成员,但在构建对象图期间,这些成员将不可避免地为
null

归根结底,这是一个非常普遍的问题。是的,它会影响反序列化,但也会影响对象的创建,例如数据传输对象或视图模型的映射或数据绑定。通常,在构造对象和设置其属性之间的很短时间内,这些成员将为null。其他时候,它们可能会在较长的时间内处于不确定状态,因为您的代码(例如)完全填充依赖项数据集,这是使用互连对象图所需的

幸运的是,微软已经解决了这个问题,为我们提供了两种不同的方法

选项#1:空原谅运算符 例如,第一种方法是使用。然而,不太明显的是,您可以直接在不可为空的成员上使用它,从而完全消除中间类(例如,在您的示例中,
SerializedPerson
)。事实上,根据您的具体业务需求,您可以将
Person
类简化为以下简单内容:

班级人员
{
内部人员(){}
公共字符串名称{get;internal set;}=null!;
公共IReadOnlyList Friends{get;internal set;}=null!;
}
选项2:可为空的属性 更新:从2021年3月9日发布的.NET 5.0.4(SDK 5.0.201)开始,以下方法现在将产生<
public class PersonForSerialize
{
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
    [Obsolete("Only intended for de-serialization.", true)]
    public PersonForSerialize()
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
    {
    }

    // Optional constructor
    public PersonForSerialize(string name, IReadOnlyList<string> friends)
    {
        Name = name;
        Friends = friends;
    }

    public string Name { get; set; }
    public IReadOnlyList<string> Friends { get; set; }
}
public List<Person> Friends { get; set; } = new List<Friends>();
using Newtonsoft.Json; 

class Person
{
    [JsonConstructor]
    internal Person(
        string name, 
        List<Person> friends)
    {
        Name = name; // Can end up being null if left without '?? "No name"';
        Friends = friends ?? new List<Person>();
    }

    public string Name { get; }

    public List<Person> Friends { get; set; }

    public void AddFriends(IEnumerable<Person> friends)
    {
        Friends.AddRange(friends);
    }
}
Deserialise<Person>("{}");
Deserialise<Person>("{ 'Name': 'John'}");
Deserialise<Person>("{ 'Name': 'John', 'Friends': [] }");
Deserialise<Person>("{ 'Name': 'John', 'Friends': [ {'Name': 'Jenny'}]}");
Deserialise<Person>("{ 'Friends': [{'Name': 'Jenny'}]}");