简单继承:序列化为类型,反序列化为子类型抛出InvalidCastException

本文关键字:类型 InvalidCastException 反序列化 序列化 继承 简单 | 更新日期: 2023-09-27 18:17:27

protobuf-net.2.1.0

我的理解是,protobuf-net完全基于接收端可用的信息来确定反序列化的消息契约——不依赖于序列化的数据包本身来构建消息契约。具体地说,类成员属性指明了期望在包中找到的字段的数据类型和顺序。

因此,由于发送端独立于接收端,因此应该可以将任何序列化的数据包解释为特定类型的字段数据&订单匹配接收方合同原型。

特别是在继承方面,应该可以序列化基本类型的对象,并将其反序列化为子类型的对象——前提是继承被正确地标记。

然而,对于一个简单的继承层次DerivedClass : BaseClass,我发现如果我序列化为BaseClass并反序列化为DerivedClass,返回的对象将是BaseClass类型。

下面是类:

[ProtoBuf.ProtoInclude(1000, typeof(DerivedClass))]
[ProtoBuf.ProtoContract]
public class BaseClass
{
    [ProtoBuf.ProtoMember(1, IsRequired = false, Name = @"Name", DataFormat = ProtoBuf.DataFormat.TwosComplement)]
    public string Name { get; set; }
}
[ProtoBuf.ProtoContract]
public class DerivedClass : BaseClass
{
    [ProtoBuf.ProtoMember(2, IsRequired = false, Name = @"Index", DataFormat = ProtoBuf.DataFormat.TwosComplement)]
    public int Index { get; set; }
}

执行以下测试方法:

public class TestClass
{
    public static void Test()
    {
        var baseObject = new BaseClass { Name = "BaseObject" };
        var derivedObject = new DerivedClass { Name = "DerivedObject", Index = 1 };
        using (var stream = new MemoryStream())
        {
            ProtoBuf.Serializer.Serialize(stream, baseObject);
            Debug.WriteLine(stream.Length);
            stream.Seek(0, SeekOrigin.Begin);
            // either of next two lines will throw the invalid cast exception : 
            // DerivedClass derivedObjectOut = ProtoBuf.Serializer.Deserialize<DerivedClass>(stream);
            // var objectOut = ProtoBuf.Serializer.Deserialize<DerivedClass>(stream);
            // no exception thrown but internal type of objectOut is unexpectedly BaseClass : 
            var objectOut = ProtoBuf.Serializer.Deserialize<DerivedClass>(stream);
        }
    }
}

产生异常:

类型为'System '的异常。InvalidCastException'发生在但在用户代码中未处理

附加信息:无法强制转换类型的对象"protobuf_net.lib.ProtoClasses。SimpleBaseClass'的类型"protobuf_net.lib.ProtoClasses.SimpleDerivedClass"。

简单继承:序列化为类型,反序列化为子类型抛出InvalidCastException

似乎在protobuf-net中有一个限制或错误。TypeModel.DeserializeCore()的工作方式是,它找到契约类型,开始将其反序列化为该类型,当遇到派生类型的标记时,切换到反序列化该类型。最后,构造和填充一个观察到的类型的对象,导致您看到的问题,因为您想要的派生类型的标记从未被观察到。

幸运的是,有一个简单的解决方案:使用Serializer.Merge<T>()将流合并到DerivedType的预分配实例中:

var baseObject = new BaseClass { Name = "BaseObject" };
using (var stream = new MemoryStream())
{
    ProtoBuf.Serializer.Serialize(stream, baseObject);
    Debug.WriteLine(stream.Length);
    stream.Seek(0, SeekOrigin.Begin);
    var derivedObjectOut = ProtoBuf.Serializer.Merge(stream, new DerivedClass());
}

它有一点代码气味,但解决了问题。

顺便说一下,在你的原始示例代码中,你试图读取两次流而不倒带。这也会抛出一个类似的异常,因为第二次调用没有遇到标记。

如果您正在编写泛型反序列化代码,您可以测试继承层次结构中是否存在ProtoIncludeAttribute,如果存在则使用以下帮助方法调用Merge():

public static class ProtobufExtensions
{
    public static T DeserializeOrMerge<T>(Stream stream)
    {
        if (!typeof(T).IsValueType
            && typeof(T) != typeof(string)
            // Test to make sure T has a public default constructor
            && typeof(T).GetConstructor(Type.EmptyTypes) != null
            && typeof(T).HasProtoIncludeAtributes())
        {
            return ProtoBuf.Serializer.Merge(stream, Activator.CreateInstance<T>());
        }
        else
        {
            return ProtoBuf.Serializer.Deserialize<T>(stream);
        }
    }
    public static bool HasProtoIncludeAtributes(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (!type.IsDefined(typeof(ProtoContractAttribute)))
            return false;
        return type.BaseTypesAndSelf().SelectMany(t => t.GetCustomAttributes<ProtoIncludeAttribute>()).Any();
    }
    public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }
}

然后像这样使用:

var baseObject = new BaseClass { NameInBaseClass = "BaseObject" };
using (var stream = new MemoryStream())
{
    ProtoBuf.Serializer.Serialize(stream, baseObject);
    Debug.WriteLine(stream.Length);
    stream.Seek(0, SeekOrigin.Begin);
    var derivedObjectOut = ProtobufExtensions.DeserializeOrMerge<DerivedClass>(stream);
}