解决实现ISerializable接口的对象中的循环引用问题

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情况下,一旦解析目标引用,我可以简单地使用反射重新访问“pending”对象字段。但是,对于实现ISerializable的对象,不可能使用SerializationInfo注入新数据。
有人能指点我正确的方向吗?
2个回答

5
这种情况是使用FormatterServices.GetUninitializedObject方法的原因。一般的想法是,如果你有对象A和B,在它们的SerializationInfo中相互引用,你可以按照以下方式反序列化它们:
(为了说明,(SI,SC)指的是类型的反序列化构造函数,即接受SerializationInfoStreamingContext的构造函数。)
  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类可以为您完成所有这些操作(以及其他类型的修补)。然而,考虑到反序列化的复杂性,我始终发现它的文档非常不足,所以从来没有花时间去尝试如何正确使用它。它使用一些更多的魔法来使用CLR内部反射优化调用(SI,SC)构造函数,从而更快地完成步骤4(我计时大约比公共方式快两倍)。
最后,存在涉及循环的对象图无法反序列化的情况。一个例子是当你有两个 IObjectReference 实例的循环时(我已经在这方面测试了 BinaryFormatter 并且它会抛出异常)。另一个我怀疑的情况是如果你有一个仅涉及装箱值类型的循环

0
你需要检测在对象图中是否使用了同一个对象超过一次,对输出中的每个对象进行标记。当出现第二次或更多时,你需要输出一个“引用”指向已存在的标记,而不是再次输出该对象。
序列化的伪代码如下:
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

例如,当您完全反序列化对象后,不能将其存储到标签存储中,因为在反序列化过程中可能需要对其进行引用。


我理解你关于“引用标签”的观点,我的格式化程序已经使用了这种技术。因此,你的自引用示例对我来说不是问题。但我不明白你的回答如何帮助我处理相互引用的ISerializable实现对象。你能否针对这个具体问题给出解决方案?谢谢。 - Chris
不是很确定您的意思。您是否在谈论与序列化相关的私有构造函数的使用? - Lasse V. Karlsen
是的,没错。唯一膨胀实现ISerializable接口的对象的方法就是调用它的特殊构造函数。 - Chris
我不知道那是如何工作的。你要么需要“反序列化”一个代理对象(我不知道如何做到这一点),要么你需要构造一个空对象只是为了获得一个对象引用,在调用实际的构造函数之前(我也不知道如何做到这一点)。 - Lasse V. Karlsen
根据BinaryFormatter的行为方式,需要创建并传递对象引用。随后,在同一对象引用上调用ISerializable构造函数。不过,目前还不清楚这是如何实现的。 - Chris
实际上,我知道。ConstructorInfo.Invoke()有一个重载形式,支持目标。 - Chris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接