В процессе разработки захотелось сериализовать синглтон, но с сохранением и восстановлением значений его полей. На первый взгляд нетрудный процесс обернулся увлекательным путешествием в сад камней и булыжников: от получения двух синглтонов до отсутствия сериализации полей.
Но зачем городить огород сад?
Первый логичный вопрос: если так проблем то зачем это вообще нужно? Такая хитрость, действительно, требуется не часто. Хотя многие люди, только начинающие работу с WPF или WinForms, пытаются реализовать таким образом файл с настройками приложения. Пожалуйста, не тратьте свое время и не изобретайте велосипед: для этого есть Application и User Settings (почитать про это можно здесь и здесь). Вот примеры, когда сериализация может потребоваться:
Хочется передать синглтон по сети или между AppDomain. К примеру, клиент и сервер одновременно работают с одним и тем же ftp и синхронизируют свои сведения о нем. Информацию об ftp можно хранить в синглтоне (и там же пристроить методы для работы с ним).
Сериализуется класс, которой присваивается различным элементам, но значение должно быть одинаковым для всех. Примером такого класса может являться DBNull.
Синглтон
В качестве несложного примера возьмем такой синглтон:
public sealed class Settings : ISerializable
{
private static readonly Settings Inst = new Settings();
private Settings()
{
}
public static Settings Instance
{
get { return Inst; }
}
public String ServerAddress
{
get { return _servAddr; }
set { _servAddr = value; }
}
public String Port
{
get { return _port; }
set { _port = value; }
}
private String _port = "";
}
Сразу сделаю несколько комментариев по коду:
- Намерено отсутствуют ленивые вычисления, чтобы не усложнять код.
- Свойства не могут быть сделаны автоматическими, т.к. имена скрытых полей класса генерируются снова при каждой компиляции, и однажды честно сериализованный объект может уже не десериализоваться по причине различия этих имен.
Первый взгляд
В простых случаях для сериализации в C# хватает добавить атрибут Serializable. Что ж, не будем сильно задумываться на сколько наш случай сложен и добавим этот атрибут. Теперь попробуем сериализовать наш синглтон в трех вариантах: SOAP, Binary и обычный XML.
Для примера сериализуем и десериализуем бинарно (остальные способы аналогичны):
using (var mem = new MemoryStream())
{
var serializer = new BinaryFormatter();
serializer.Serialize(mem, Settings.Instance);
mem.Seek(0, SeekOrigin.Begin);
object o = serializer.Deserialize(mem);
Console.WriteLine((Settings)o == Settings.Instance);
}
(Не)ожиданно на консоль будет выведено false, а это значит, что мы получили два объекта-синглтона. Такой результат можно предвидеть, если вспомнить, что в процессе десериализации с помощью рефлексии вызывается приватный конструктор и все десериализуемые значения присваиваются новому объекту. Именно эта особенность синглтона кладет первый камень в наш сад: синглтон перестает быть синглтоном.
Усложняем и… кладем еще каменей.
Так как не получилось сделать все просто, придется усложнить Если обратимся к более “ручному” процессу сериализации через интерфейс ISerializable, то на первый взгляд выгоды кажется никакой: прошлая беда не исчезла, а сложность возросла. Поэтому для дальнейшей действий нам еще потребуется достаточно редко используемый интерфейс IObjectReference. Все что он делает: показывает что объект класса, реализующего этот интерфейс, указывает на другой объект. Звучит странно, не правда ли? Но нам нужна другая особенность: после десериализации такого объекта будет возвращен указатель не на него самого, а на тот объект, на который он указывает. В нашем случае логично было бы возвращать указатель на синглтон. Класс будет выглядеть так:
[Serializable]
internal sealed class SettingsSerializationHelper : IObjectReference
{
public Object GetRealObject(StreamingContext context)
{
return Settings.Instance;
}
}
Теперь мы можем сериализовывать объект класса SettingsSerializationHelper, а при десериализации получать Settings.Instance. Правда здесь есть два еще два камня:
- Перед тем как сериализовать синглтон требуется создать объект другого класса.
- Поля синглтона по-прежнему не сериализуются.
Рассмотрим первый камень, который не очень критичен, но явно не приятен. Решение проблемы заключено в подмене класса для сериализации внутри GetObjectData. Выглядеть это будет так (внутри синглтона):
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(SettingsSerializationHelper));
}
Теперь когда мы будем сериализовывать синглтон вместо него будет сохранен объект SettingsSerializationHelper, а при десериализации мы получим обратно наш синглтон. Проверив вывод на консоль из ранее описанного примера сериализации, мы увидим, что в случае с Binary и SOAP будет выведено на консоль true, но для XML сериализации — false. Следовательно, XMLSerializer не вызывает GetObjectData и просто самостоятельно обрабатывает все public поля/свойства.
Грязные хаки
Проблема с сериализацией полей — самый крупный камень в нашем саду. К сожалению, мне не удалось найти совсем элегантное и честное решение, но получилось соорудить не очень честный, достаточно гибкий “хак”.
Для начала в методе GetObjectData добавим сохранение полей синглтона. Выглядеть это будет так:
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(SettignsSerializeHelper));
info.AddValue("_servAddr", ServerAddressr);
info.AddValue("_port", Port);
}
Если теперь сделать SOAP сериализацию, то можно увидеть, что все поля действительно сериализованны. Однако в действительности мы сериализовывали SettignsSerializationHelper, у которого эти поля отсутствуют, а значит при десериализации у возникнут проблемы. Есть два пути решения:
- Полностью повторить все поля синглтона в SettignsSerializationHelper. Такую подмену десериализатор вполне скушает, заполнит все поля, а внутри метода GetRealObject их надо обратно присвоить синглтону. У такого подхода есть один большой и серьёзный недостаток: ручная поддержка дублирования полей, их добавление для сериализации и десериализации. Это явно не наш
бровыбор. - Призвать на помощь рефлексию, суррогатный селектор и чуточку linq, чтобы все было сделано за нас. Рассмотрим это подробнее.
В начале изменим метод GetObjectData:
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof (SettignsSerializeHelper));
var fields = from field in typeof (Settings).GetFields(BindingFlags.Instance |
BindingFlags.NonPublic | BindingFlags.Public)
where field.GetCustomAttribute(typeof (NonSerializedAttribute)) == null
select field;
foreach (var field in fields)
{
info.AddValue(field.Name, field.GetValue(Settings.Instance));
}
}
Отлично, теперь когда мы захотим добавить поле в синглтон оно будет тоже сериалзованно без работы руками. Перейдем к десериализации.
Все поля синглтона должны быть повторены в SettignsSerializationHelper, но для того, чтобы избежать их реального дублирования, применим суррогатный селектор и изменим SettignsSerializationHelper.
Новый SettignsSerializationHelper:
[Serializable]
internal sealed class SettignsSerializeHelper : IObjectReference
{
public readonly Dictionary<String, object> infos =
(from field in typeof (Settings).GetFields(BindingFlags.Instance
| BindingFlags.NonPublic | BindingFlags.Public)
where field.GetCustomAttribute(typeof (NonSerializedAttribute)) == null
select field).ToDictionary(x => x.Name, x => new object());
public object GetRealObject(StreamingContext context)
{
foreach (var info in infos)
{
typeof (Settings).GetField(info.Key, BindingFlags.Instance | BindingFlags.NonPublic
| BindingFlags.Public).SetValue(Settings.Instance, info.Value);
}
return Settings.Instance;
}
}
И так, внутри SettignsSerializationHelper создается хэш-мап, где key — имена сериализуемых полей, а value в будущем станут значениями этих полей после десериалазации. Здесь для большей инкапсуляции можно сделать infos как private и написать метод для доступка к его key-value парам, но мы не будем усложнять пример. Внутри GetRealObject мы устанавливаем синглтону его десериализованные значения полей и возвращаем ссылку на него.
Теперь осталось только заполнить infos значениями полей. Для этого будет использован селектор.
internal sealed class SettingsSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
throw new NotImplementedException();
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context,
ISurrogateSelector selector)
{
var ssh = new SettignsSerializeHelper();
foreach (var val in info)
{
ssh.infos[val.Name] = val.Value;
}
return ssh;
}
}
Так как селектор будет использоваться только для десериализации, то мы напишем только SetObjectData. Когда obj (десериализуемый объект) приходит внутрь селектора, его поля заполнены 0 и null не зависимо от обстоятельств (obj получается после вызова в процессе десериализации метода GetUninitializedObject из FormatterServices). Поэтому в нашем случае проще создать новый SettignsSerializationHelper и вернуть его (этот объект будет считаться десериализованным). Далее, внутри foreach заполняем infos десериализованными данными, которые потом будут присвоены полям синглтона.
И теперь пример самого процесса сериализации/десериализации:
И теперь пример самого процесса сериализации/десериализации:
using (var mem = new MemoryStream())
{
var soapSer = new SoapFormatter();
soapSer.Serialize(mem, Settings.Instance);
var ss = new SurrogateSelector();
ss.AddSurrogate(typeof(SettignsSerializeHelper),
soapSer.Context, new SettingsSurrogate());
soapSer.SurrogateSelector = ss;
mem.Seek(0, SeekOrigin.Begin);
var o = soapSer.Deserialize(mem);
Console.WriteLine((Settings)o == Settings.Instance);
}
На консоль будет выведено true и все поля будут восстановлены. Наконец, мы закончили и привели наш сад камней в должный вид.
Автор: Tronok