在 C# 8.0 中反序列化对象时处理临时空引用类型? [英] Handling temporarily null reference types when deserializing objects in C# 8.0?

查看:27
本文介绍了在 C# 8.0 中反序列化对象时处理临时空引用类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

解决方法

我已经能够使用几种不同的技术来解决这个问题,并且它们都按预期工作.然而,他们还通过引入大量额外的脚手架来显着扩展代码.

选项 #1:影子类

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

选项 #2:影子成员

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

选项 #3:反思

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

但是当我引入可空字段时,我的解析代码会显着增加 - 并且其唯一目的是满足代码分析.

示例代码

为了演示,我尽量简化了代码;我的实际课程显然远不止这些.

class Person{私有 IReadOnlyList?朋友们;内部人员(字符串名称){this.Name = 名称;}公共字符串名称 { 获取;}public IReadOnlyList朋友 =>这个.朋友们!内部 SetFriends(IReadOnlyListfriends){this.friends = 朋友;}}类 PersonForSerialize{公共字符串?名称 { 得到;放;}公共 IReadOnlyList朋友{得到;放;}}IReadOnlyListLoadPeople(字符串路径){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){人[]朋友=新人[peopleTemp[i].Friends.Count];for (int j = 0; j 

问题

有没有一种方法可以满足 C# 8.0 中对反序列化期间仅临时为 null 的属性的空引用检查,而无需为每个类引入大量额外代码?

解决方案

您担心虽然您的对象不打算具有 null 成员,但那些成员在构建对象图的过程中将不可避免地 null.

最终,这是一个非常常见的问题.是的,它会影响反序列化,但也会影响在例如映射或数据绑定期间的对象创建.数据传输对象或视图模型.通常,这些成员在构造对象和设置其属性之间的很短的时间内为空.其他时候,他们可能会在更长的时间内处于不确定状态,例如您的代码.完全填充依赖数据集,这里需要你的互连对象图.

幸运的是,微软已经解决了这个确切场景,为我们提供了两种不同的方法.

选项 #1:Null-Forgiving Operator

第一种方法,正如 @andrew-hanlon 在他的回答中所指出的,是使用 null-forgiving operator.然而,可能不是很明显的是,您可以直接在不可为空的成员上使用它,从而完全消除您的中间类(例如,PersonForSerialize 在您的示例中).事实上,根据您的具体业务需求,您可以将 Person 类缩减为如下简单的内容:

class Person{内部人员(){}公共字符串名称 { 获取;内部设置;} = 空!;public IReadOnlyList朋友{得到;内部设置;} = 空!;}

选项 #2:可空属性

<块引用>

更新:从 2021 年 3 月 9 日发布的 .NET 5.0.4(SDK 5.0.201)开始,以下方法现在将生成 CS8616 警告.鉴于此,您最好使用上面概述的 null-forgiving 运算符.

第二种方法为您提供完全相同的结果,但通过 可空属性.这些需要比 null-forgiving 运算符更多的注释,但也更明确地说明正在发生的事情.事实上,我实际上更喜欢这种方法只是因为它对于不习惯语法的开发人员来说更加明显和直观.

class Person{内部人员(){}[NotNull, DisallowNull]公共字符串?名称 { 得到;内部设置;};[NotNull, DisallowNull]公共 IReadOnlyList?朋友{得到;内部设置;};}

在这种情况下,您通过向返回添加可空性指示符 (?) 来明确承认成员可以null类型(例如,IReadOnlyList?).但是您然后使用可为空属性告诉消费者即使成员被标记为可为空:

  • [NotNull]:可空返回值永远为空.
  • [DisallowNull]:输入参数不应为空.

分析

无论您使用哪种方法,最终结果都是一样的. 如果在不可为空的属性上没有空值原谅运算符,您的成员会收到以下警告:><块引用>

CS8618:不可为空的属性名称"未初始化.考虑将属性声明为可为空.

或者,如果不对可为空的属性使用 [NotNull] 属性,则在尝试将其值分配给不可为空的变量时,您会收到以下警告:

<块引用>

CS8600:将空文字或可能的空值转换为不可为空的类型.

或者,类似地,在尝试调用值的成员时:

<块引用>

CS8602:取消引用可能为空的引用.

然而,使用这两种方法之一,您可以使用默认 (null) 值构造对象,同时仍然让下游消费者相信这些值实际上不是 null——因此,允许他们使用这些值而无需保护条款或其他防御性代码.

相反,当使用这些方法中的任何一种时,当您尝试为这些成员分配 null 值时,您仍会收到以下警告:

<块引用>

CS8625:无法将空文字转换为不可为空的引用类型.

没错:您甚至可以在分配给 string? 属性 时得到它,因为这是 [DisallowNull] 指示编译器来做.

结论

这取决于您采用哪种方法.由于它们都产生相同的结果,这纯粹是一种风格偏好.无论哪种方式,您都可以在构造过程中保持成员 null,同时仍然可以实现 C# 不可空类型的好处.

I am trying C# 8.0 out, and I want to enable the null reference checking for the entire project. I am hoping I can improve my code design, and without disabling the nullability context in any code scopes.

I encountered a problem when I deserialize an object graph. The objects have references with one another but, for the final user view, all references in the object graph must have a value.

In other words, during the deserialization process, references may be null, but after all objects have finished loading, a final process will link all of the objects together, thus resolving those null references.

Workarounds

I have been able to address this using a few different techniques, and they each work as expected. They also expand the code considerably, however, by introducing a lot of extra scaffolding.

Option #1: Shadow Class

For example, I tried writing a paired class for each kind of object, using these as an intermediate object during deserialization. In these paired classes, all reference are allowed to be null. After deserialization completes, I copy all fields from these classes and convert them to the real object. Of course, with this approach, I need write a lot of extra code.

Option #2: Shadow Members

Alternatively, I tried to put a nullable field and a non-nullable property. This is similar to the previous approach, but I'm using paired members instead of paired classes. Then I add an internal setter for each field. This approach has less code than the first, but it still increases my code base considerably.

Option #3: Reflection

Traditionally, without considering performance, I would have managed deserialization using reflection so that there’s almost has no extra code on a per class basis. But writing my own parsing code has some benefits—for example, I can output more useful error messages including tips on how callers can resolve issues.

But when I introduce the nullable fields, my parsing code increases considerably—and with the sole purpose of satisfying the code analysis.

Sample Code

For the sake of demonstration, I simplified the code as much as possible; my actual classes obviously do much more than this.

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);
    }
}

Question

Is there a way to satisfy the null reference checking in C# 8.0 for properties that are only temporarily null during deserialization without introducing a lot of extra code for every class?

解决方案

You’re concerned that while your objects aren't intended to have null members, those members will inevitably be null during the construction of your object graph.

Ultimately, this is a really common problem. It affects, yes, deserialization, but also the creation of objects during e.g., mapping or data binding of e.g. data transfer objects or view models. Often, these members are to be null for a very brief period between constructing an object and setting its properties. Other times, they might sit in limbo during a longer period as your code e.g. fully populates a dependency data set, as required here with your interconnected object graph.

Fortunately, Microsoft has addressed this exact scenario, offering us two different approaches.

Option #1: Null-Forgiving Operator

The first approach, as @andrew-hanlon notes in his answer, is to use the null-forgiving operator. What may not be immediately obvious, however, is that you can use this directly on your non-nullable members, thus entirely eliminating your intermediary classes (e.g., PersonForSerialize in your example). In fact, depending on your exact business requirements, you might be able to reduce your Person class down to something as simple as:

class Person
{

    internal Person() {}

    public string Name { get; internal set; } = null!; 

    public IReadOnlyList<Person> Friends { get; internal set; } = null!;

}

Option #2: Nullable Attributes

Update: As of .NET 5.0.4 (SDK 5.0.201), which shipped on March 9th, 2021, the below approach will now yield a CS8616 warning. Given this, you are better off using the null-forgiving operator outlined above.

The second approach gives you the same exact results, but does so by providing hints to Roslyn's static flow analysis via nullable attributes. These require more annotations than the null-forgiving operator, but are also more explicit about what's going on. In fact, I actually prefer this approach just because it's more obvious and intuitive to developers otherwise unaccustomed to the syntax.

class Person
{

    internal Person() {}

    [NotNull, DisallowNull]
    public string? Name { get; internal set; }; 

    [NotNull, DisallowNull]
    public IReadOnlyList<Person>? Friends { get; internal set; };

}

In this case, you're explicitly acknowledging that the members can be null by adding the nullability indicator (?) to the return types (e.g., IReadOnlyList<Person>?). But you're then using the nullable attributes to tell consumers that even though the members are marked as nullable:

  • [NotNull]: A nullable return value will never be null.
  • [DisallowNull]: An input argument should never be null.

Analysis

Regardless of which approach you use, the end results are the same. Without the null-forgiving operator on a non-nullable property, you would have received the following warning on your members:

CS8618: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.

Alternatively, without using the [NotNull] attribute on a nullable property, you would have received the following warning when attempting to assign its value to a non-nullable variable:

CS8600: Converting null literal or possible null value to non-nullable type.

Or, similarly, upon trying to call a member of the value:

CS8602: Dereference of a possibly null reference.

Using one of these two approaches, however, you can construct the object with default (null) values, while still giving downstream consumers confidence that the values will, in fact, not be null—and, thus, allowing them to consume the values without necessitating guard clauses or other defensive code.

Conversely, when using either of these approaches, you will still get the following warning when attempting to assign a null value to these members:

CS8625: Cannot convert null literal to non-nullable reference type.

That's right: You'll even get that when assigning to the string? property because that's what the [DisallowNull] is instructing the compiler to do.

Conclusion

It’s up to you which of these approaches you take. As they both yield the same results, it’s a purely stylistic preference. Either way, you’re able to keep the members null during construction, while still realizing the benefits of C#’s non-nullable types.

这篇关于在 C# 8.0 中反序列化对象时处理临时空引用类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆