9

protobuf-net を使用して複雑なオブジェクト グラフをシリアライズ/デシリアライズしようとすると、いくつかの問題が発生します。

私はレガシー アプリケーションに取り組んでおり、.Net Remoting を使用して GUI クライアントを C# サービスに接続しています。デフォルトの を使用したオブジェクト グラフのシリアル化されたサイズが原因で、海外のユーザーのパフォーマンスが低下していますBinaryFormatter。これは、クライアントとサーバーの間の帯域幅の制限 (1Mbit/s) によって悪化しています。

手っ取り早い解決策として、代わりに protobuf-net を使用してISerializable. テスト中に、オブジェクト参照が維持されないという問題に遭遇しました。

問題を再現する例をまとめました。(Items[1]) 内のオブジェクトとオブジェクト BA は、属性で指定したものDictionaryと同じになると 予想しています。AsReference=trueProtoMember

を使用する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>();
    }
}
4

3 に答える 3

4

Edit: this should work from the next build onwards simply by marking the type's AsReferenceDefault:

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

At the current time this is sort of an unsupported scenario - at least, via the attributes it is unsupported; basically, the AsReference=true currently is referring to the KeyValuePair<int,A>, which doesn't really make sense since KeyValuePair<int,A> is a value-type (so this can never be treated as a reference; I've added a better message for that in my local copy).

Because KeyValuePair<int,A> acts (by default) as a tuple, there is currently nowhere to support the AsReference information, but that is a scenario I would like to support better, and I will be investigating this.

There was also a bug that meant that AsReference on tuples (even reference-type tuples) was getting out-of-order, but I've fixed that locally; this was where the "changed" message came from.

In theory, the work for me to do this isn't huge; the fundamentals already work, and oddly enough it came up separately on twitter last night too - I guess "dictionary pointing to an object" is a very common scenario. At a guess, I imagince I'll add some atribute to help describe this situation, but you can actually hack around it at the moment using a couple of different routes:

1: configure KeyValuePair<int,A> manually:

[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);
}

I don't like this much, because it exploits implementation details of KeyValuePair<,> (the private fields), and may not work between .NET versions. I would prefer to replace KeyValuePair<,> on the fly via a surrogate:

[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);
    }
}

This configures something to use instead of KeyValuePair<int,A> (converted via the operators).

In both of these, Execute is just:

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]);
}

I do, however, want to add direct support. The good thing about both of the above is that when I get time to do that, you just have to remove the custom configuration code.

For completeness, if your code is using Serializer.* methods, then rather than create / configure a new model, you should configure the default model:

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

Serializer.* is basically a short-cut to RuntimeTypeModel.Default.*.

Finally: you should not create a new TypeModel per call; that would hurt prerformance. You should create and configure one model instance, and re-use it lots. Or just use the default model.

于 2013-01-22T09:58:02.230 に答える
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;    
于 2014-02-24T01:21:55.510 に答える