将C#8.0可为空的引用类型与反序列化一起使用时的最佳实践? [英] Best practice when using C# 8.0 nullable reference types with deserialization?

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

问题描述

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

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.

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

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

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

I have been able to address this using a couple of different techniques, and they both work as expected. They also expand the code considerably, however, and they don’t seem especially elegant.

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

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.

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

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.

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

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. I want to know whether it is a good and proper approach to do this?

为了演示,我尽可能地简化了代码.我实际的课程显然要做的比这更多.我想知道是否有更好的代码样式.

For the sake of demonstration, I simplified the code as much as possible; my actual classes obviously do much more than this. I want to known whether there is any better code style.

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

推荐答案

您担心的是,虽然您的对象不是有意具有 null 成员,但这些成员在构建对象图期间,将不可避免地为 null .

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.

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

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

第一种方法,如 @ andrew-hanlon在其答案中指出的内容,是使用

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., SerializedPerson 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!;

}

选项#2:可空属性

第二种方法可以为您提供相同的精确结果,但是可以通过

Option #2: Nullable Attributes

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

}

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

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] :可为空的返回值将从不为空.
  • [DisallowNull] :输入参数永远不能为空.
  • [NotNull]: A nullable return value will never be null.
  • [DisallowNull]: An input argument should never be null.

无论使用哪种方法,最终结果都是相同的.如果在不可为null的属性上没有可为null的运算符,则您的成员将收到以下警告:

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 :不可空的属性名称"未初始化.考虑将属性声明为可为空.

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

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

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 :将空文字或可能的空值转换为不可空类型.

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

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

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

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

CS8602: Dereference of a possibly null reference.

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

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.

相反,当使用这两种方法之一时,尝试为这些成员分配 null 值时,您将仍然收到以下警告:

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 :无法将空文字转换为不可为空的引用类型.

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

是的:分配给 string?属性时,您甚至会得到,因为这正是 [DisallowNull] 所指示的编译器要做.

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.

由您决定采用哪种方法.由于它们都产生相同的结果,因此纯粹是风格上的偏爱.无论哪种方式,您都可以在构造期间将成员保持为 null ,同时仍然可以实现C#的非空类型的好处.

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天全站免登陆