使用循环引用序列化对象图的方法

Approaches to Serializing an Object Graph with Circular References

假设为了练习,我想实现一个序列化程序 (C#),并且我希望所述序列化程序不会因循环引用而失败。

明显的解决方案是仅序列化尚未遇到的对象并跳过遇到的对象。这很容易通过散列实例(以一种或另一种方式)来实现。

建议的解决方案出价问题:"What defines an object's identity?" 有人会说 - 将其留给 GetHashCode 和 Equals 方法。 这是一个可接受的解决方案 - 它可以节省序列化时间并节省反序列化内存。

然而,这并不总是一个理想的结果,因为许多实例可能具有相同的标识,但在序列化域中用于完全不同的事物,因此稍后将它们作为同一实例反序列化将违反域逻辑。

因此,作为此类序列化程序的作者,我必须让调用者做出此类决定。

解决此问题的一种方法是对每个所述类型的集合进行哈希处理,并通过迭代集合并对每个包含的元素调用 ReferenceEquals 来区分序列化和非序列化实例。 这可行,但不是最佳的 - 性能明智。

另一种方法是将对象固定在非托管堆中,并将固定的对象地址用作标识,这似乎有些矫枉过正,而且开销也很大。

另一种方法是使用反射来调用每个实例的 Object.Equals 和 Object.GetHashCode 默认实现 - 这似乎可以解决问题,但其开销很小。

我的问题是:

1)对于我所建议的方法,是否有任何我遗漏的注意事项?
2)还有没有其他我可能没有想到的方法?

唯一真正会导致循环引用(即您的应用程序中无休止的循环)的是实际的对象引用。所以不要保留哈希列表,保留以前遇到的对象本身的列表。

如果你想保持序列化数据尽可能小,你可以实现它类似于 nuget 组织包文件夹的方式 - 将每个对象写出一次,但在一个对象引用另一个对象的地方,写一些引用键排序。

[
    {
        serialisationKey: "GUID1",
        name: "Neil",
        friends: [
            { obj: "GUID2" },
            { obj: "GUID3" }
        ]
    },
    {
        serialisationKey: "GUID2",
        name: "Bob",
        friends: [
            { obj: "GUID1" }
        ]
    },
    {
        serialisationKey: "GUID3",
        name: "Alf",
        friends: [
            { obj: "GUID1" }
        ]
    }
]

不要固定到内存!您可以使用 object.ReferenceEquals

您的序列化程序不应该很聪明,并试图弄清楚是否需要将同一对象序列化为一个对象或两个对象。序列化每个对象一次 - 如果对象被引用两次,则在序列化数据中引用它两次。

看看System.Runtime.Serialization.ObjectIDGenerator。它正是这样做的。

根据 MSDN 页面:

Using a hash table, the ObjectIDGenerator retains which ID is assigned to which object. The object references, which uniquely identify each object, are addresses in the runtime garbage-collected heap. Object reference values can change during serialization, but the table is updated automatically so the information is correct.

源代码也可用here