【问题标题】:Inheritance in protobuf.net, adding a lower base class still backward compatible?protobuf.net 中的继承,添加较低的基类仍然向后兼容?
【发布时间】:2016-11-15 11:30:34
【问题描述】:

我使用 protobuf.net 已经有一段时间了,它非常棒。我可以有一个从基类继承的类,我可以通过在基类中使用 ProtoInclude 语句来序列化派生类。如果我的基类在对象被序列化时最初只说两个 ProtoInclude 语句,那么说

[ProtoInclude(100, typeof(Vol_SurfaceObject))]
[ProtoInclude(200, typeof(CurveObject))]
internal abstract class MarketDataObject 

我仍然可以将同一个对象反序列化为已演变为具有更多派生的代码:

[ProtoInclude(100, typeof(Vol_SurfaceObject))]
[ProtoInclude(200, typeof(CurveObject))]
[ProtoInclude(300, typeof(StaticDataObject))]
internal abstract class MarketDataObject 

到目前为止一切顺利(事实上非常好,感谢 Marc)。但是,现在如果我想要一个比我当前的基类更低的基类(在本例中为 MarketDataObject)怎么办。这样我就有了

[ProtoInclude(100, typeof(Vol_SurfaceObject))]
[ProtoInclude(200, typeof(CurveObject))]
[ProtoInclude(300, typeof(StaticDataObject))]
internal abstract class MarketDataObject : LowerStillBaseClass
{ blah }

[ProtoInclude(10, typeof(MarketDataObject))]
internal abstract class LowerStillBaseClass
{ blah }

虽然代码当然可以工作,但当对象只有 2 个 ProtoInclude 语句时,我是否仍然能够反序列化初始对象到这种新形式的 MarketDataObject 类?

【问题讨论】:

    标签: c# serialization protobuf-net


    【解决方案1】:

    这不适用于纯静态 protbuf-net 属性。稍微简化一下,假设您从以下内容开始:

    namespace V1
    {
        [ProtoContract]
        internal class MarketDataObject
        {
            [ProtoMember(1)]
            public string Id { get; set; }
        }
    }
    

    并将其重构为以下内容:

    namespace V2
    {
        [ProtoInclude(10, typeof(MarketDataObject))]
        [ProtoContract]
        internal abstract class LowerStillBaseClass
        {
            [ProtoMember(1)]
            public string LowerStillBaseClassProperty { get; set; }
        }
    
        [ProtoContract]
        internal class MarketDataObject : LowerStillBaseClass
        {
            [ProtoMember(1)]
            public string Id { get; set; }
        }
    }
    

    接下来,尝试将一个从 V1 类创建的反序列化为 V2 类。您将失败并出现以下异常:

    ProtoBuf.ProtoException: No parameterless constructor found for LowerStillBaseClass
    

    原因这不起作用是类型层次结构是先序列化的,而不是先派生的。要查看这一点,请通过调用 Console.WriteLine(RuntimeTypeModel.Default.GetSchema(type)); 转储每种类型的 protobuf-net 合约,对于 V1.MarketDataObject,我们得到:

    message MarketDataObject {
       optional string Id = 1;
    }
    

    对于V2.MarketDataObject

    message LowerStillBaseClass {
       optional string LowerStillBaseClassProperty = 1;
       // the following represent sub-types; at most 1 should have a value
       optional MarketDataObject MarketDataObject = 10;
    }
    message MarketDataObject {
       optional string Id = 1;
    }
    

    MarketDataObject 被编码成message,其基类型字段首先位于顶层,然后派生类型字段递归封装在嵌套可选消息中,其中字段 id 表示其子类型。因此,当 V1 消息反序列化为 V2 对象时,不会遇到子类型字段,不会推断出正确的派生类型,并且派生类型值会丢失。

    一种解决方法是避免使用[ProtoInclude(10, typeof(MarketDataObject))],而是使用RuntimeTypeModel API以编程方式在派生类型的契约中填充基类成员:

    namespace V3
    {
        [ProtoContract]
        internal abstract class LowerStillBaseClass
        {
            [ProtoMember(1)]
            public string LowerStillBaseClassProperty { get; set; }
        }
    
        [ProtoContract]
        internal class MarketDataObject : LowerStillBaseClass
        {
            static MarketDataObject()
            {
                AddBaseTypeProtoMembers(RuntimeTypeModel.Default);
            }
    
            const int BaseTypeIncrement = 11000;
    
            public static void AddBaseTypeProtoMembers(RuntimeTypeModel runtimeTypeModel)
            {
                var myType = runtimeTypeModel[typeof(MarketDataObject)];
                var baseType = runtimeTypeModel[typeof(MarketDataObject).BaseType];
                if (!baseType.GetSubtypes().Any(s => s.DerivedType == myType))
                {
                    foreach (var field in baseType.GetFields())
                    {
                        myType.Add(field.FieldNumber + BaseTypeIncrement, field.Name);
                    }
                }
            }
    
            [ProtoMember(1)]
            public string Id { get; set; }
        }
    }
    

    (这里我在 MarketDataObject 的静态构造函数中填充合同。您可能想在其他地方执行此操作。)V3. 的架构如下所示:

    message MarketDataObject {
       optional string Id = 1;
       optional string LowerStillBaseClassProperty = 11001;
    }
    

    此模式与 V1 模式兼容,因此可以将 V1 消息反序列化为 V3 类而不会丢失数据。示例fiddle

    当然,如果您要将成员从 MarketDataObject 移动到 LowerStillBaseClass,则需要确保字段 ID 保持不变。

    此解决方法的缺点是您无法反序列化 LowerStillBaseClass 类型的对象,并让 protobuf-net 自动推断正确的派生类型。

    【讨论】:

    • 感谢您非常详细的回答以及您花费的时间。你说的很有道理。
    猜你喜欢
    • 1970-01-01
    • 2016-02-13
    • 2014-02-25
    • 2022-09-27
    • 1970-01-01
    • 2014-04-19
    • 2018-05-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多