【问题标题】:Protobuf-net object reference deserialization using Dictionary: A reference-tracked object changed reference during deserializationProtobuf-net object reference deserialization using Dictionary: A reference-tracked object changed reference during deserialization
【发布时间】:2013-01-04 09:18:18
【问题描述】:

我在尝试使用 protobuf-net 序列化/反序列化复杂对象图时遇到一些问题。

我正在开发一个遗留应用程序,我们正在使用 .Net Remoting 将 GUI 客户端连接到 C# 服务。由于我们使用默认 BinaryFormatter 的对象图的序列化大小,我们看到海外用户的性能不佳,而客户端和服务器之间的有限带宽 (1Mbit/s) 加剧了这种情况。

作为一个快速的胜利,我想我会整理一个概念证明,看看通过使用 protobuf-net 是否有任何性能提升,通过实现 ISerializable。在我进行测试时,我遇到了一个没有维护对象引用的问题。

我已经整理了一个可以重现该问题的示例。我希望Dictionary (Items[1]) 中的对象和 B.A 对象与我在ProtoMember 属性中指定的AsReference=true 相同。

使用protobuf-net 2.0.0.619,我看到反序列化时抛出异常(反序列化期间引用跟踪的对象更改了引用)。

如果这不是受支持的方案,请告诉我。

测试

[Test]
public void AreObjectReferencesSameAfterDeserialization()
{
    A a = new A();
    B b = new B();

    b.A = a;

    b.Items.Add(1, a);

    Assert.AreSame(a, b.A);
    Assert.AreSame(b.A, b.Items[1]);

    B deserializedB;

    using (var stream = new MemoryStream())
    {
        Serializer.Serialize(stream, b);
        stream.Seek(0, SeekOrigin.Begin);
        deserializedB = Serializer.Deserialize<B>(stream);
    }

    Assert.AreSame(deserializedB.A, deserializedB.Items[1]);
}

类定义

[Serializable]
[ProtoContract]
public class A
{
}

[Serializable]
[ProtoContract]
public class B
{
    [ProtoMember(1, AsReference = true)]
    public A A { get; set; }

    [ProtoMember(2, AsReference = true)]
    public Dictionary<int, A> Items { get; set; }

    public B()
    {
        Items = new Dictionary<int, A>();
    }
}

【问题讨论】:

  • 看起来其他人也有类似的问题:stackoverflow.com/questions/6923891/… 标准列表和字典工作正常;但是,我有一个未完成的更改请求来支持字典中的 AsReference。意思是,Dictionary 目前不会运行 Foo 的图形代码,但如果它给你带来很大的痛苦,我可能会花一些时间来看看这个如果它对我很有帮助,如果图形代码确实为 Foo 运行 :)
  • 作为一个更新:我已经努力让它以一种不需要大惊小怪的方式工作。我还有一个问题需要解决,目前的行为取决于发现类型的顺序(这显然是非常不可取的),我需要解决这个问题。
  • 好东西,很高兴听到您能够如此迅速地完成某些工作。
  • AsReferenceDefault 上的可选属性属性 ProtoContract 将在下一个版本中完全正常工作

标签: c# serialization protobuf-net


【解决方案1】:

编辑:这应该可以从下一个构建开始,只需标记类型的AsReferenceDefault

[ProtoContract(AsReferenceDefault=true)]
public class A
{
    // ...
}

目前这是某种不受支持的场景 - 至少,通过属性它是不受支持的;基本上,AsReference=true 当前 指的是KeyValuePair&lt;int,A&gt;,这实际上没有意义,因为KeyValuePair&lt;int,A&gt; 是一个值类型(所以这永远不能被视为参考;我'在我的本地副本中添加了更好的消息)。

因为KeyValuePair&lt;int,A&gt;(默认情况下)作为一个元组,目前没有地方支持AsReference信息,但这是我想更好地支持的场景,我会正在调查此事。

还有一个错误意味着元组(甚至是引用类型的元组)上的AsReference 出现故障,但我已经在本地修复了它;这就是“已更改”消息的来源。

理论上,我做这件事的工作量并不大; 基本原理 已经起作用了,奇怪的是,它昨晚也在 twitter 上单独出现了——我猜“字典指向一个对象”是一个非常常见的场景。猜测一下,我想我会添加一些属性来帮助描述这种情况,但你现在实际上可以使用几种不同的路线来解决它:

1:手动配置KeyValuePair&lt;int,A&gt;

[Test]
public void ExecuteHackedViaFields()
{
    // I'm using separate models **only** to keep them clean between tests;
    // normally you would use RuntimeTypeModel.Default
    var model = TypeModel.Create();

    // configure using the fields of KeyValuePair<int,A>
    var type = model.Add(typeof(KeyValuePair<int, A>), false);
    type.Add(1, "key");
    type.AddField(2, "value").AsReference = true;

     // or just remove AsReference on Items
    model[typeof(B)][2].AsReference = false;

    Execute(model);
}

我不太喜欢这个,因为它利用了KeyValuePair&lt;,&gt;(私有字段)的实现细节,并且可能无法在.NET 版本之间工作。我希望通过 代理 即时替换 KeyValuePair&lt;,&gt;

[Test]
public void ExecuteHackedViaSurrogate()
{
    // I'm using separate models **only** to keep them clean between tests;
    // normally you would use RuntimeTypeModel.Default
    var model = TypeModel.Create();

    // or just remove AsReference on Items
    model[typeof(B)][2].AsReference = false;

    // this is the evil bit: configure a surrogate for KeyValuePair<int,A>
    model[typeof(KeyValuePair<int, A>)].SetSurrogate(typeof(RefPair<int, A>));
    Execute(model);
}

[ProtoContract]
public struct RefPair<TKey,TValue> {
    [ProtoMember(1)]
    public TKey Key {get; private set;}
    [ProtoMember(2, AsReference = true)]
    public TValue Value {get; private set;}
    public RefPair(TKey key, TValue value) : this() {
        Key = key;
        Value = value;
    }
    public static implicit operator KeyValuePair<TKey,TValue>
        (RefPair<TKey,TValue> val)
    {
        return new KeyValuePair<TKey,TValue>(val.Key, val.Value);
    }
    public static implicit operator RefPair<TKey,TValue>
        (KeyValuePair<TKey,TValue> val)
    {
        return new RefPair<TKey,TValue>(val.Key, val.Value);
    }
}

这会配置一些东西以使用 而不是 KeyValuePair&lt;int,A&gt;(通过运算符转换)。

在这两个中,Execute 只是:

private void Execute(TypeModel model)
{
    A a = new A();
    B b = new B();

    b.A = a;

    b.Items.Add(1, a);

    Assert.AreSame(a, b.A);
    Assert.AreSame(b.A, b.Items[1]);

    B deserializedB = (B)model.DeepClone(b);

    Assert.AreSame(deserializedB.A, deserializedB.Items[1]);
}

不过,我确实想添加直接支持。上述两个方面的好处是,当我有时间这样做时,您只需删除自定义配置代码。

为了完整起见,如果您的代码使用 Serializer.* 方法,那么您应该配置 default 模型,而不是创建/配置 new 模型:

RuntimeTypeModel.Default.Add(...); // etc

Serializer.* 基本上是RuntimeTypeModel.Default.* 的捷径。

最后:你不应该在每次调用时创建一个新的TypeModel;那会损害性能。您应该创建和配置一个模型实例,并多次重复使用它。或者只使用默认模型。

【讨论】:

  • 感谢 Marc,这是一个非常有用且全面的答案。我采用了代理方法,它运行良好,但是正如您所说,我认为这种情况相对常见,因此很高兴看到它支持开箱即用,而无需为每个 KVP 配置代理。
  • @Lee 确实;这样做的另一个原因是“预编译”目前仅适用于基于属性的模型:它不执行检查程序集中的任何代码。这限制了在“轻量级”框架(WP7/WP8/iOS/等)上的使用。
【解决方案2】:

我设置了一个小测试,发现 AsReferenceDefault 属性并没有像预期的那样工作。

测试类:

[ProtoContract(AsReferenceDefault = true)]
public class TEST
{
    [ProtoMember(1018)]
    public List<TEST> _Items { get; set; }

    [ProtoMember(1001, AsReference = true)]
    public TEST Parent;

    [ProtoMember(1003)]
    public string NameItemType;

    public void AddItem(TEST Item)
    {
        _Items.Add(Item);
        Item.Parent = this;
    }

    public TEST()
    {
    }
}

测试代码:

        TEST ci = new TEST(); ci._Items = new List<TEST>(); ci.NameItemType = "ROOT_ITEM";
        TEST ci_2 = new TEST(); ci_2._Items = new List<TEST>(); ci_2.NameItemType = "ITEM_02"; ci.AddItem(ci_2);
        TEST ci_3 = new TEST(); ci_3._Items = new List<TEST>(); ci_3.NameItemType = "ITEM_03"; ci_2.AddItem(ci_3);

        // --> Confirm references.
        bool AreEqual = false;
        if (ci == ci_2.Parent)
            AreEqual = true;
        if (ci_2 == ci_3.Parent)
            AreEqual = true;

        // --> Serialize.
        byte[] buf;
        using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
        {
            ProtoBuf.Serializer.Serialize(ms, ci);
            buf = ms.ToArray();
        }

        // --> Deserialize.
        using (System.IO.MemoryStream ms = new System.IO.MemoryStream(buf))
        {
            ci = ProtoBuf.Serializer.Deserialize<TEST>(ms);
        }

        // --> Confirm references.
        ci_2 = ci._Items[0];  
        ci_3 = ci_2._Items[0];
        if (ci == ci_2.Parent)
            AreEqual = true;
        if (ci_2 == ci_3.Parent)  // HERE IS WHERE IT FAILS! 
                                  // THEY SHOULD BE EQUAL AFTER DESERIALIZATION!
            AreEqual = true;    

【讨论】:

    【解决方案3】:

    更新给那些可能遇到类似问题的人:从版本 2.3.0 开始,不需要使用上面提到的任何技巧 Marc。一切都按照 Topic Starter 的要求进行:

    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void AreObjectReferencesSameAfterDeserialization()
        {
            A a = new A();
            B b = new B();
    
            b.A = a;
    
            b.Items.Add( 1, a );
    
            Assert.AreSame( a, b.A );
            Assert.AreSame( b.A, b.Items[ 1 ] );
    
            B deserializedB;
    
            var model = TypeModel.Create();
    
            using( var stream = new MemoryStream() )
            {
                model.Serialize( stream, b );
                stream.Seek( 0, SeekOrigin.Begin );
                deserializedB = (B) model.Deserialize( stream, null, typeof(B) );
            }
    
            Assert.AreSame( deserializedB.A, deserializedB.Items[ 1 ] );
        }
    }
    
    [ProtoContract]
    public class A
    {
    }
    
    [ProtoContract]
    public class B
    {
        [ProtoMember( 1, AsReference = true )]
        public A A { get; set; }
    
        [ProtoMember( 2, AsReference = true )]
        public Dictionary<int, A> Items { get; set; }
    
        public B()
        {
            Items = new Dictionary<int, A>();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-04-13
      • 1970-01-01
      • 2019-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-02
      相关资源
      最近更新 更多