protobuf-net:如何序列化一个空的列表

32

我们在序列化空列表方面遇到了一些问题。以下是使用 CF 2.0 的 .NET 代码示例:

//Generating the protobuf-msg
ProtoBufMessage msg = new ProtoBufMessage();
msg.list = new List<AnotherProtobufMessage>();
// Serializing and sending throw HTTP-POST
MemoryStream stream = new MemoryStream();
Serializer.Serialize(stream, msg);
byte[] bytes = stream.ToArray();
HttpWebRequest request = createRequest();
request.ContentLength = bytes.Length ;

using (Stream httpStream = request.GetRequestStream())
{              
      httpStream.Write(bytes, 0, bytes.Length);
}

当我们尝试在流上写入数据时,出现了异常(bytes.length超出范围)。但一个空列表的类型不应该是0字节,对吧(类型信息?)?

我们需要这种类型的发送,因为响应中包含了服务器发给客户端的消息。

3个回答

40

这个协议(定义由 Google,不在我控制之内!)只发送有关于的数据。它不区分列表和null列表。所以,如果没有要发送的数据 - 是的,长度为0(这是一种非常节俭的格式 ;-p)。

在传输过程中,协议缓冲区不包含任何类型元数据。

还有一个常见的坑点是,您可能会认为列表属性会自动实例化为空,但实际上并不会这样(除非您的代码执行它,例如在字段初始化程序或构造函数中)。

下面是一个可行的hack:

[ProtoContract]
class SomeType {

    [ProtoMember(1)]
    public List<SomeOtherType> Items {get;set;}

    [DefaultValue(false), ProtoMember(2)]
    private bool IsEmptyList {
        get { return Items != null && Items.Count == 0; }
        set { if(value) {Items = new List<SomeOtherType>();}}
    }
}

可能有点取巧,但应该可以用。如果你愿意的话,也可以省略Items的 "set",只需删除bool:

    [ProtoMember(1)]
    public List<SomeOtherType> Items {get {return items;}}
    private readonly List<SomeOtherType> items = new List<SomeOtherType>();

    [DefaultValue(false), ProtoMember(2)]
    private bool IsEmptyList {
        get { return items.Count == 0; }
        set { }
    }

1
虽然Google以那种方式定义了序列化,但这并不意味着它是合理的(对于某些情况可能是,对于其他情况可能不是)。如果我们使用protobuf来持久化对象层次结构,人们会期望在序列化和反序列化时得到其层次结构的副本而没有任何差异。我认为添加一个选项来强制序列化空集合以便在反序列化时重新创建相同的对象层次结构将是一个很好的功能。我认为问题的赞数应该在一定程度上证明我的请求;-) - Eric Ouellet
另一个更通用的功能是将方法标记为“[OnDeserialized]”,并在该对象上的序列化完成后调用。 - Eric Ouellet
我的前两条评论是基于我理解的序列化处理空集合和处理null集合的方式相同(都是不进行序列化)。 - Eric Ouellet
1
@EricOuellet:“我认为添加一个选项来强制序列化空集合以便在反序列化时重新创建相同的对象层次结构将是一个很棒的功能。” 这是一个有趣的目标,但在数据协议中实际上没有表达这种方式的方法;集合本身不会出现在流中 - 只有内容,因此如果没有内容... - Marc Gravell
1
感谢Marc的回答。我觉得这真的很令人难过。根据我的期望和大多数人的期望,序列化程序应该负责持久化对象层次结构的确切状态。所有使用Protobuf并且有一个或多个空集合的人都必须编写额外的代码,而这些代码本来不应该存在于一个完全功能的序列化程序中。我非常喜欢你的序列化程序(性能很棒),但是那种“错误行为”真的削弱了我使用它的热情。 - Eric Ouellet

3
如@Marc所说,线路格式只发送项目数据,因此为了知道列表是否为空或null,您必须向流添加该信息。 添加额外的属性以指示原始集合是否为空很容易,但如果您不想修改原始类型定义,则有另外两个选项:

使用代理序列化

代理类型将具有额外的属性(保持原始类型不变),并将恢复列表的原始状态:null,带有项目或为空。
    [TestMethod]
    public void SerializeEmptyCollectionUsingSurrogate_RemainEmpty()
    {
        var instance = new SomeType { Items = new List<int>() };

        // set the surrogate
        RuntimeTypeModel.Default.Add(typeof(SomeType), true).SetSurrogate(typeof(SomeTypeSurrogate));

        // serialize-deserialize using cloning
        var clone = Serializer.DeepClone(instance);

        // clone is not null and empty
        Assert.IsNotNull(clone.Items);
        Assert.AreEqual(0, clone.Items.Count);
    }

    [ProtoContract]
    public class SomeType
    {
        [ProtoMember(1)]
        public List<int> Items { get; set; }
    }

    [ProtoContract]
    public class SomeTypeSurrogate
    {
        [ProtoMember(1)]
        public List<int> Items { get; set; }

        [ProtoMember(2)]
        public bool ItemsIsEmpty { get; set; }

        public static implicit operator SomeTypeSurrogate(SomeType value)
        {
            return value != null
                ? new SomeTypeSurrogate { Items = value.Items, ItemsIsEmpty = value.Items != null && value.Items.Count == 0 }
                : null;
        }

        public static implicit operator SomeType(SomeTypeSurrogate value)
        {
            return value != null
                ? new SomeType { Items = value.ItemsIsEmpty ? new List<int>() : value.Items }
                : null;
        }
    }

使您的类型可扩展

protobuf-net建议使用IExtensible接口,允许您扩展类型,以便在消息中添加字段而不会出现任何问题(在此处阅读更多信息:这里)。为了使用protobuf-net扩展,您可以继承Extensible类或实现IExtensible接口,以避免继承约束。
现在,您的类型是“可扩展的”,您可以定义[OnSerializing][OnDeserialized]方法来添加新的指标,当使用其原始状态重构对象时,这些指标将被序列化到流中并从中反序列化。
优点是您不需要定义新属性或新类型作为代理,缺点是如果您的类型模型中定义了子类型,则不支持IExtensible

    [TestMethod]
    public void SerializeEmptyCollectionInExtensibleType_RemainEmpty()
    {
        var instance = new Store { Products = new List<string>() };

        // serialize-deserialize using cloning
        var clone = Serializer.DeepClone(instance);

        // clone is not null and empty
        Assert.IsNotNull(clone.Products);
        Assert.AreEqual(0, clone.Products.Count);
    }

    [ProtoContract]
    public class Store : Extensible
    {
        [ProtoMember(1)]
        public List<string> Products { get; set; }

        [OnSerializing]
        public void OnDeserializing()
        {
            var productsListIsEmpty = this.Products != null && this.Products.Count == 0;
            Extensible.AppendValue(this, 101, productsListIsEmpty);
        }

        [OnDeserialized]
        public void OnDeserialized()
        {
            var productsListIsEmpty = Extensible.GetValue<bool>(this, 101);
            if (productsListIsEmpty)
                this.Products = new List<string>();
        }
    }

0
public List<NotificationAddress> BccAddresses { get; set; }

你可以替换为:

private List<NotificationAddress> _BccAddresses;
public List<NotificationAddress> BccAddresses {
   get { return _BccAddresses; }
   set { _BccAddresses = (value != null && value.length) ? value : null; }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接