Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Singleton serialization либо сад камней

Anna | 18.06.2014 | нет комментариев
В процессе разработки захотелось сериализовать синглтон, но с сохранением и поправлением значений его полей. На 1-й взор нетрудный процесс обернулся интересным путешествием в сад камней и булыжников: от приобретения 2-х синглтонов до отсутствия сериализации полей.

Но для чего городить огород сад?

1-й логический вопрос: если так задач то для чего это вообще необходимо? Такая хитрость, подлинно, требуется не Зачастую. Правда многие люди, только начинающие работу с 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 = "";
}

Сразу сделаю несколько комментариев по коду:

  • Намерено отсутствуют ленивые вычисления, Дабы не усложнять код.
  • Свойства не могут быть сделаны механическими, т.к. имена спрятанных полей класса генерируются вновь при всякой компиляции, и некогда Добросовестно сериализованный объект может теснее не десериализоваться по причине отличия этих имен.
1-й взор

В примитивных случаях для сериализации в C# хватает добавить признак Serializable. Что ж, не будем крепко задумываться на сколько наш случай труден и добавим данный признак. Сейчас испробуем сериализовать наш синглтон в 3 вариантах: SOAPBinary и обыкновенный 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, а это значит, что мы получили два объекта-синглтона. Такой итог дозволено предвидеть, если припомнить, что в процессе десериализации с поддержкой рефлексии вызывается приватный конструктор и все десериализуемые значения присваиваются новому объекту. Именно эта специфика синглтона кладет 1-й камень в наш сад: синглтон перестает быть синглтоном.

Усложняем и… кладем еще каменей.

Так как не получилось сделать все легко, придется усложнить Если обратимся к больше “ручному” процессу сериализации через интерфейс ISerializable, то на 1-й взор выгоды кажется никакой: прошлая напасть не исчезла, а трудность усилилась. Следственно для последующей действий нам еще понадобится довольно редко применяемый интерфейс IObjectReference. Все что он делает: показывает что объект класса, реализующего данный интерфейс, указывает на иной объект. Звучит необычно, не правда ли? Но нам необходима иная специфика: позже десериализации такого объекта будет возвращен указатель не на него самого, а на тот объект, на тот, что он указывает. В нашем случае разумно было бы возвращать указатель на синглтон. Класс будет выглядеть так:

[Serializable]
internal sealed class SettingsSerializationHelper : IObjectReference 
{
    public Object GetRealObject(StreamingContext context) 
    {
        return Settings.Instance;
    }
}

Сейчас мы можем сериализовывать объект класса SettingsSerializationHelper, а при десериализации получатьSettings.Instance. Правда тут есть два еще два камня:

  • Перед тем как сериализовать синглтон требуется сделать объект иного класса.
  • Поля синглтона по-бывшему не сериализуются.

Разглядим 1-й камень, тот, что не дюже критичен, но очевидно не славен. Решение задачи заключено в подмене класса для сериализации внутри 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 и все поля будут восстановлены. Наконец, мы завершили и привели наш сад камней в надлежащий вид.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB