Protobuf.net - class 的所有字段都需要序列化

Protobuf.net - all fields of a class required to be serialized

这里有一篇example class provided by Marc Gravel介绍他如何使用Protobuf.net:

[ProtoContract]
class Person {
    [ProtoMember(1)]
    public int Id {get;set;}
    [ProtoMember(2)]
    public string Name {get;set;}
    [ProtoMember(3)]
    public Address Address {get;set;}
}
[ProtoContract]
class Address {
    [ProtoMember(1)]
    public string Line1 {get;set;}
    [ProtoMember(2)]
    public string Line2 {get;set;}
}

我有一些问题,在网上搜索后找不到答案:

  1. 如果在第 1 天,我知道我不需要名称 属性 [ProtoMember(2)],那么如果我省略 [ProtoMember(2)] 属性, Protobut.net 会忽略 属性 并且不将其包含在输出序列化数据中吗?如果为真,那么当数据在另一端被反序列化时 - Name 初始化为什么 - null?

  2. 假设所有 3 个属性最初都已序列化,如上所示。如果将来发现不再需要名称 属性 [ProtoMember(2)],是否可以安全地省略 [ProtoMember(2)] 属性,以便只有第一个和第三个 属性 连载了吗?如果为真,是否可以简单地保留所示的属性编号(即 1 和 3)?如果是这种情况,有什么注意事项吗?

  3. 如果可以在 class 中省略 属性 的序列化属性,那么如果反序列化端的 class 定义是不同步?比如反序列化class定义了上面所有的3个属性,但是序列化代码只定义了1和3?同样,如果反序列化代码只希望看到属性 1 和 3,但序列化代码发送所有 3,这仍然有效还是会产生错误?

  1. 正确;就初始化为什么而言——这通常取决于你的类型,所以 在这种情况下 (因为你的类型没有初始化它),是的: null (注意: 还有一个选项可以抑制构造函数,在这种情况下它将是 null 即使 你的 class 有一个构造函数/初始化器)
  2. 是的,这很好(并且符合预期)
  3. 属性的添加和删除是正常的,所以图书馆可以原谅;当收到意外字段时,接下来会发生什么取决于您的 class 是否实现了 IExtensible(通常是通过子classing Extensible)——如果是这样,意外数据将被存储分开,这样它仍然可以手动查询,或者(更常见)"round tripped"(即如果你再次序列化它,额外的数据会被持久化,即使你没有预料到它)
  4. (是的,我知道你没有问 4)- 该库还支持 "conditional serialization",可以根据条件省略 where-by 属性 - 例如 public bool ShouldSerializeName() => Name != null && SomethingElse == 42;

由于其他人可能想知道这些问题的答案,所以我决定 post 这个问题并分享我的发现。

这些问题实际上很容易用测试程序解决:

class Program
{
    static void Main(string[] args)
    {
        var person = new Person1
        {
            Id = 12345,
            Name = "Fred",
            Address = new Address
            {
                Line1 = "Flat 1",
                Line2 = "The Meadows"
            }
        };
        //
        byte[] arr = Serialize(person);
        Person2 newPerson = Deserialize(arr);
        /*
        using (var file = File.Create("person.bin"))
        {
            Serializer.Serialize(file, person);
        }
        //
        Person newPerson;
        using (var file = File.OpenRead("person.bin"))
        {
            newPerson = Serializer.Deserialize<Person>(file);
        }
        */
    }

    public static byte[] Serialize(Person1 person)
    {
        byte[] result;
        using (var stream = new MemoryStream())
        {
            Serializer.Serialize(stream, person);
            result = stream.ToArray();
        }
        return result;
    }

    public static Person2 Deserialize(byte[] tData)
    {
        using (var ms = new MemoryStream(tData))
        {
            return Serializer.Deserialize<Person2>(ms);
        }
    }
}

[ProtoContract]
class Person1
{
    [ProtoMember(1)]
    public int Id { get; set; }
    [ProtoMember(2)]
    public string Name { get; set; }
    [ProtoMember(3)]
    public Address Address { get; set; }
}
[ProtoContract]
class Address
{
    [ProtoMember(1)]
    public string Line1 { get; set; }
    [ProtoMember(2)]
    public string Line2 { get; set; }
}

[ProtoContract]
class Person2
{
    [ProtoMember(1)]
    public int Id { get; set; }
    [ProtoMember(2)]
    public string Name { get; set; }
    [ProtoMember(3)]
    public Address Address { get; set; }
}

因此,要尝试在序列化时 Protobuf.net 是否会忽略名称 属性,首先 运行 调试代码以查看所有 3 个属性序列化的总字节数.这可以通过在 Serialize(person) 行设置断点并检查 arr.

的大小来完成

然后,从 Person1 class 的名称 属性 中删除 [ProtoMember(2)] 属性。 运行 debug 中的代码显示它省略了它,因为字节数比以前少了 6 个。然后将对象反序列化回 Person2 的对象时,它会显示 Name 属性 被初始化为 null。

替换 Person1 class 中的 Name 属性 的 [ProtoMember(2)] 属性后,删除 Person2 class 的相同属性。通过代码调试后,显示Deserialize调用后,Person2.Name 属性设置为null。

因此,看起来 Protobuf.net 设计得非常灵活、高效,并且在某些方面是向后兼容的,因为它支持删除过时的属性。