3

我正在编写自己的 IFormatter 实现,但我想不出一种方法来解决两种都实现 ISerializable 的类型之间的循环引用。

这是通常的模式:

[Serializable]
class Foo : ISerializable
{
    private Bar m_bar;

    public Foo(Bar bar)
    {
        m_bar = bar;
        m_bar.Foo = this;
    }

    public Bar Bar
    {
        get { return m_bar; }
    }

    protected Foo(SerializationInfo info, StreamingContext context)
    {
        m_bar = (Bar)info.GetValue("1", typeof(Bar));
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("1", m_bar);
    }
}

[Serializable]
class Bar : ISerializable
{
    private Foo m_foo;

    public Foo Foo
    {
        get { return m_foo; }
        set { m_foo = value; }
    }

    public Bar()
    { }

    protected Bar(SerializationInfo info, StreamingContext context)
    {
        m_foo = (Foo)info.GetValue("1", typeof(Foo));
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("1", m_foo);
    }
}

然后我这样做:

Bar b = new Bar();
Foo f = new Foo(b);
bool equal = ReferenceEquals(b, b.Foo.Bar); // true

// Serialise and deserialise b

equal = ReferenceEquals(b, b.Foo.Bar);

如果我使用开箱即用的 BinaryFormatter 对 b 进行序列化和反序列化,则上述对引用相等性的测试会如预期的那样返回 true。但我无法想出在我的自定义 IFormatter 中实现这一点的方法。

在非 ISerializable 情况下,一旦解决了目标引用,我就可以简单地使用反射重新访问“待定”对象字段。但是对于实现 ISerializable 的对象,不可能使用 SerializationInfo 注入新数据。

谁能指出我正确的方向?

4

2 回答 2

5

这种情况是该FormatterServices.GetUninitializedObject方法的原因。一般的想法是,如果您有对象 A 和 B 在它们的 中相互引用SerializationInfo,则可以按如下方式对它们进行反序列化:

(为了解释的目的,(SI,SC)指的是类型的反序列化构造函数,即接受 aSerializationInfo和 a的构造函数StreamingContext。)

  1. 首先选择一个要反序列化的对象。只要您不选择值类型,您选择哪个都没有关系。假设您选择A。
  2. 调用GetUninitializedObject分配(但不初始化)A 类型的实例,因为您还没有准备好调用它的(SI,SC)构造函数。
  3. 以通常的方式构建 B,即创建一个SerializationInfo对象(其中将包含对现在半反序列化的 A 的引用)并将其传递给 B 的(SI,SC)构造函数。
  4. 现在您拥有了初始化分配的 A 对象所需的所有依赖项。创建它的SerializationInfo对象并调用 A 的(SI,SC)构造函数。您可以通过反射在现有实例上调用构造函数。

GetUninitializedObject方法是纯粹的 CLR 魔术——它创建一个实例而无需调用构造函数来初始化该实例。它基本上将所有字段设置为零/空。

这就是警告您不要在(SI,SC)构造函数中使用子对象的任何成员的原因 - 子对象可能已分配但此时尚未初始化。这也是IDeserializationCallback接口的原因,它让您有机会在保证完成所有对象初始化之后并且在返回反序列化对象图之前使用您的子对象。

ObjectManager类可以为您完成所有这些(以及其他类型的修复)。但是,考虑到反序列化的复杂性,我一直发现它的文档很少,所以我从来没有花时间尝试弄清楚如何正确使用它。它使用更多的魔法来执行第 4 步,使用一些内部到 CLR 的反射进行优化,以(SI,SC)更快地调用构造函数(我已经将它的计时速度大约是公共方式的两倍)。

最后,有些对象图涉及无法反序列化的循环。一个例子是当你有两个IObjectReference实例的循环时(我已经BinaryFormatter对此进行了测试,它会引发异常)。我怀疑的另一个问题是,如果您有一个只涉及盒装 value-types 的循环

于 2010-09-07T13:54:46.793 回答
0

您需要检测您在对象图中多次使用同一个对象,在输出中标记每个对象,当您遇到 #2 或更高的情况时,您需要输出对现有标签的“引用”对象再次。

序列化伪代码:

for each object
    if object seen before
        output tag created for object with a special note as "tag-reference"
    else
        create, store, and output tag for object
        output tag and object

反序列化的伪代码:

while more data
    if reference-tag to existing object
        get object from storage keyed by the tag
    else
        construct instance to deserialize into
        store object in storage keyed by deserialized tag
        deserialize object

请务必按照指定的顺序执行最后一步,以便您可以正确处理这种情况:

SomeObject obj = new SomeObject();
obj.ReferenceToSomeObject = obj;    <-- reference to itself

IE。完全反序列化后,您无法将对象存储到标签存储中,因为在反序列化时可能需要在存储中引用它。

于 2010-04-26T10:26:05.647 回答