Разрешение циркулярных ссылок для объектов, реализующих ISerializable
Я пишу свою собственную реализацию 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 ответа
Эта ситуация является причиной FormatterServices.GetUninitializedObject
метод. Общая идея заключается в том, что если у вас есть объекты A и B, которые ссылаются друг на друга в своих SerializationInfo
Вы можете десериализовать их следующим образом:
(Для целей этого объяснения, (SI,SC)
ссылается на конструктор десериализации типа, то есть тот, который принимает SerializationInfo
и StreamingContext
.)
- Выберите один объект для десериализации первым. Неважно, какой вы выбираете, если вы не выбираете тот, который является типом значения. Допустим, вы выбираете А.
- Вызов
GetUninitializedObject
выделить (но не инициализировать) экземпляр типа A, потому что вы еще не готовы вызвать его(SI,SC)
конструктор. - Построить Б обычным способом, то есть создать
SerializationInfo
объект (который будет включать в себя ссылку на теперь половину десериализованной A) и передать его в B(SI,SC)
конструктор. - Теперь у вас есть все зависимости, необходимые для инициализации выделенного объекта A. Создать это
SerializationInfo
возражать и вызывать А(SI,SC)
конструктор. Вы можете вызвать конструктор в существующем экземпляре через отражение.
GetUninitializedObject
Метод является чистой магией CLR - он создает экземпляр без вызова конструктора для его инициализации. Это в основном устанавливает все поля в ноль / ноль.
Это причина, по которой вас предупреждают, что вы не должны использовать какие-либо элементы дочернего объекта в (SI,SC)
Конструктор - дочерний объект может быть выделен, но еще не инициализирован в этой точке. Это также причина IDeserializationCallback
интерфейс, который дает вам возможность использовать ваши дочерние объекты после того, как гарантированно будет выполнена вся инициализация объекта и до возвращения десериализованного графа объекта.
Класс ObjectManager может сделать все это (и другие типы исправлений) за вас. Тем не менее, я всегда считал, что он недостаточно документирован, учитывая сложность десериализации, поэтому я никогда не тратил время на то, чтобы понять, как правильно его использовать. Для выполнения шага 4 используется больше магии, используется некоторое внутреннее отражение в CLR, оптимизированное для вызова (SI,SC)
Конструктор быстрее (я рассчитал это примерно в два раза быстрее, чем общедоступный способ).
Наконец, существуют графы объектов, включающие циклы, которые невозможно десериализовать. Одним из примеров является, когда у вас есть цикл из двух IObjectReference
экземпляры (я проверял BinaryFormatter
на этом и выкидывает исключение). Я подозреваю, что если у вас есть цикл, включающий только типы значений в штучной упаковке.
Вам необходимо обнаружить, что вы использовали один и тот же объект более одного раза в графе объектов, пометить каждый объект в выходных данных, и когда вы достигнете значения #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
то есть. Вы не можете сохранить объект в вашем хранилище тегов после того, как полностью его десериализовали, так как вам может понадобиться ссылка на него в хранилище, пока вы десериализуете его.