Разрешение циркулярных ссылок для объектов, реализующих ISerializable - PullRequest
3 голосов
/ 26 апреля 2010

Я пишу свою собственную реализацию 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.

Кто-нибудь может указать мне правильное направление?

Ответы [ 2 ]

5 голосов
/ 07 сентября 2010

Эта ситуация является причиной для метода FormatterServices.GetUninitializedObject. Общая идея заключается в том, что если у вас есть объекты A и B, которые ссылаются друг на друга в их SerializationInfo, вы можете десериализовать их следующим образом:

(Для целей этого объяснения (SI,SC) относится к конструктору десериализации типа, то есть к тому, который принимает SerializationInfo и StreamingContext.)

  1. Выберите один объект для десериализации первым. Неважно, какой вы выбираете, если вы не выбираете тот, который является типом значения. Допустим, вы выбрали A.
  2. Вызовите GetUninitializedObject , выделите (но не инициализируйте) экземпляр типа A, потому что вы еще не готовы вызвать его конструктор (SI,SC).
  3. Создайте B обычным способом, то есть создайте объект SerializationInfo (который будет включать ссылку на теперь уже частично десериализованный A) и передайте его конструктору (SI,SC) B.
  4. Теперь у вас есть все зависимости, необходимые для инициализации вашего выделенного объекта A. Создайте объект SerializationInfo и вызовите конструктор (SI,SC). Вы можете вызвать конструктор в существующем экземпляре через отражение.

Метод GetUninitializedObject является чистой магией CLR - он создает экземпляр, даже не вызывая конструктор для его инициализации. Это в основном устанавливает все поля в ноль / ноль.

По этой причине вам не рекомендуется использовать какой-либо элемент дочернего объекта в конструкторе (SI,SC) - дочерний объект может быть выделен, но еще не инициализирован в этой точке. Это также является причиной интерфейса IDeserializationCallback, который дает вам возможность использовать ваши дочерние объекты после того, как гарантированно будет выполнена вся инициализация объекта и до возвращения десериализованного графа объектов.

Класс ObjectManager может сделать все это (и другие типы исправлений) за вас. Тем не менее, я всегда считал, что он недостаточно документирован, учитывая сложность десериализации, поэтому я никогда не тратил время на то, чтобы понять, как правильно его использовать. Для выполнения шага 4 требуется больше магии, используется некоторое внутреннее отражение в CLR, оптимизированное для более быстрого вызова конструктора (SI,SC) (я рассчитал его примерно вдвое быстрее, чем общедоступный способ).

Наконец, существуют графы объектов, включающие циклы, которые невозможно десериализовать. Один пример - когда у вас есть цикл из двух IObjectReference экземпляров (я протестировал BinaryFormatter на этом, и он выдает исключение). Я подозреваю, что если у вас есть цикл , включающий только типы значений в штучной упаковке .

0 голосов
/ 26 апреля 2010

Вам необходимо обнаружить, что вы использовали один и тот же объект более одного раза в графе объектов, пометить каждый объект в выходных данных, и когда вы достигнете значения # 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

есть. вы не можете сохранить объект в вашем хранилище тегов после того, как полностью его десериализовали, поскольку вам может понадобиться ссылка на него в хранилище во время десериализации.

...