Регистрация | Вход 
Просмотр статьи
Сериализация (Serialization) & Полиморфизм

В данной статье рассмотрены два способа сериализации с сохранением реального типа объекта. Рассмотренные методы программирования позволяют использовать все преимущества полиморфизма.

Примечание.

  • Все примеры кода в статье на языке C#.
  • Все примеры кода проверены в Microsoft Visual Studio 2008.
  • Код в примерах не рассчитан на работу в реальных проектах, он создан для иллюстрации использования описываемых элементов FCL.

Введение

С проблемой, решение которой я опишу в этой статье, я столкнулся сам, когда писал одно приложение. Суть его заключалась в том, что оно «ловило» сообщения, поступающие от мыши и клавиатуры, сохраняло их, а потом могло воспроизводить действия мыши и клавиатуры на основе сохраненных данных. Так как все сообщения похожи, я определил базовый класс Event:

using System.Xml;

abstract class Event
{
    public virtual void Save(XmlWriter writer) { }
    public virtual void Start() { }
}

Далее для каждого специфического вида сообщений я определил производные классы:

class MouseEvent : Event
{
    public override void Save(XmlWriter writer)
    {/* сохраняю сообщение от мыши*/}
    public override void Start()
    {/* иммитирую сообщение от мыши*/}
}
class KeybEvent : Event
{
    public override void Save(XmlWriter writer)
    {/* сохраняю сообщение от клавиатуры*/}
    public override void Start()
    {/* имитирую сообщение от клавиатуры*/}
}

Систему приема сообщений я организовал с помощью механизма событий. И для каждого вида события я определил обработчик, который проверял того ли типа сообщение, и если тип сообшения совпадал с типом обработчика, то он создавал объект специфичного класса(MouseEvent или KeybEvent)и добавлял в общую коллекцию (подключая/отключая обработчики, я мог контролировать какие сообщения сохранять, а какие нет).

Далее везде в программе я работал (сохранял, запускал) с объектами класса Event.

Проблема

Но вот я дошел до создания того участка программы, который должен был читать сохранные данные, и на основе этих данных формировать коллекцию объектов типа Event. Сначала я написал метод, который смотрел на тип данных (при сохранении я указывал хмл атрибут Type="MouseEvent" или Type="KeybEvent") , и на основе этого создавал объект специфичного класса:

switch (Type)
{
    case "MouseEvent":
        //Collection это коллекиця объектов Event 
        Collection.Add(new MouseEvent(…); break;
    case "KeybEvent":
        Collection.Add(new KeybEvent(…)); break;
    default: break;
}

Потом я решил добавить возможность обрабатывать сообщения от колесика мыши. Для его хранения и запуска нужно было выполнять специфичные действия, и я определил еще один класс:

class MouseWheelEvent: Event
{…}

В результате мне пришлось добавить в этот switch еще один case, который создавал объект MouseWheelEvent.

Потом я посмотрел на код и понял, что надо немного изменить структуру класса KeybEvent, в результате чего параметры его конструктора изменились. И мне снова пришлось залазить в тот switch и менять код. (Причем этот switch находился в другом файле, в другой папке, и о том, что надо его модифицировать я вспомнил только благодаря отладчику :) ).

В данном примере все упрощено. Но в больших проектах данная проблема может сильно ударить по гибкости программы. Кроме того, ИМХО, такой способ чтения данных не соответствует хваленым принципам ООП.

Решение

Нужен был способ сохранять объект Event, но, не теряя его настоящего типа (MouseEvent, …). К счастью такой способ есть. Он основан на использовании сериализации. Реализовать этот способ можно несколькими путями. Во-первых, сначала нужно выбрать в каком формате мы будем сериализовать. FCL предоставляет на выбор два вида:

  • Двоичная сериализация
  • Xml сериализация

Первый способ идеально подходит для нашего случая. Он сохраняет настоящий тип сериализуемого объекта. Кроме того, он сохраняет все поля объекта.

Второй более нагляден, но менее функционален. Хмл сериализация сохраняет информацию в формате xml, а это значит, что она более доступна, ее легко изменять. Однако за эти не очень большие по сути преимущества, приходится платить. Хмл сериализация сохраняет только открытые поля и свойства! Однако можно, если немного попотеть, можно обойти это ограничение, и сериализовать то, что нам надо. Кроме того, для того чтобы сериализовать/десериализовать нам придется самим сохранять информацию о типе!

Сначала я рассмотрю первый способ - двоичную сериализацию, а потом xml.

Вариант 1. Двоичная сериализация

Сразу к делу:

using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Serialization;

[Serializable] // обратите внимание на этот атрибут, он обязателен
class MouseEvent : Event
{
    //обязательно открытый конструктор
    //Типы всех полей должны также иметь атрибут [Serializable]
    //...
}

//вот как будет выглядеть метод Save в Event
//в производных классах его не будет
[Serializable]
abstract class Event
{
    // Нет смысла переопределять этот метод в каждом классе
    public void Save()
    {
        //сериализатор
        BinaryFormatter ser = new BinaryFormatter();

        //файл куда будем сериализовать          
        Stream f = new StreamWriter("Test.gaga", false).BaseStream;

        ser.Serialize(f, this);
        f.Close();
    }
}

А вот так мы будем читать данные:

BinaryFormatter ser = new BinaryFormatter();
Stream f = new StreamReader("Test.gaga", System.Text.Encoding.Default).BaseStream;
Event ev = (Event)ser.Deserialize(f);

Как видите элементарно просто и удобно.

Кстати процесс сериализации можно контролировать. Достаточно реализовать в нашем сериализуемом классе интерфейс ISerializable:

[Serializable]
class MouseEvent : Event, ISerializable
{
    //Внимание тип сериализуемых полей должен иметь атрибут [Serializable]
    //большинство стандартных типов его имеют.
    int XCoordinate; //одно из многих полей

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.SetType(typeof(MouseEvent));

        //метод хорошо перегружен, одна из версий AddValue(String, Object, Type)
        info.AddValue("XCoordinate", XCoordinate, XCoordinate.GetType());
    }

    //обязательно должен быть конструктор с такой сигнатурой, для того, что бы при 	
    //десериализации назначить полям сохраненные при сериализации значения
    public MouseEvent(SerializationInfo info, StreamingContext context)
    {
        XCoordinate = (int)info.GetValue("XCoordinate", typeof(int));
    }
    //…
}

Казалось бы лучше некуда :-)

Но это еще не все! Можно гораздо проще контролировать процесс сериализации. Достаточно полям, которые мы не собираемся сериализовать назначить атрибут System.NonSerializedAttribute:

[field: NonSerializedAttribute()]
int YCoordinate; //это поле не будет сериализоваться(а значит оно может быть любого типа! )

И на этом не все! Если полю YCoordinate всеже при десериализации нужно начальное значение, можно определить метод, который будет вызываться при десериализации, и в нем назначить YCoordinate нужное начальное значение!

[OnDeserializedAttribute()]
private void RunThisMethod(StreamingContext context)
{
    YCoordinate = 777;
} 

Теперь все :) Лично я был приятно удивлен, когда узнал обо всех этих возможностях. Все это добро находиться в mscorlib.dll.

Вариант 2. Xml сериализация

Этот вид сериализации хорош своей простотой, т.к. в итоге получается удобный для восприятия и редактирования xml-файл. Автоматика здесь не то, что при двоичной сериализации: кроме открытых полей и свойств без вашего вмешательства ничего не сериализуется. Более того, о типе вам придется самим думать. Впрочем, сейчас вы сами все увидите. Кстати не забудьте добавить ссылку на System.Xml.dll в свой проект при работе этим способом.

Сначала посмотрим, что может сделать автоматика.

using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System;
using System.Text;

//обратите внимание, public в объявлении класса обязательно!!!
public class MouseEvent : Event
{
    public int XCoordinate = 777;
}

public abstract class Event
{
    public void Save()
    {
        //видите, конструктор XmlSerializer  требует тип 
        //сериализуемого объекта
        XmlSerializer ser = new
    XmlSerializer(this.GetType());

        //все тоже самое
        Stream f = new StreamWriter("Test.xml", false).BaseStream;
        ser.Serialize(f, this);
        f.Close();

        //а дальше нам надо как-то сохранить тип нашего объекта
        //поясню: тип объекта можно представить объектом типа Type. 
        //именно его возвращает GetType(), и именно его требует конструктор XmlSerializer
        //в классе Type есть метод GetType(string name), который позволяет на основе имени
        // типа получить соответствующий объект Type. Этим я и воспользуюсь

        StreamWriter ft = new StreamWriter("TestType.type", false);
        // this.GetType()) вернет реальный тип объекта, т.е. MouseEvent, KeybEvent… 
        ft.Write(this.GetType().AssemblyQualifiedName);
        ft.Close();
    }
}

А читать это мы будем так:

//сначала получим тип
//файл
StreamReader ft = new StreamReader("TestType.type", Encoding.Default);
//получаем тип основываясь на его имени (содержащем имя сборки)
Type type = Type.GetType(ft.ReadToEnd());

//теперь мы знаем тип, можно десериализовать сам объект
XmlSerializer ser = new XmlSerializer(type);

Stream f = new StreamReader("Test.xml", Encoding.Default).BaseStream;

//Deserialize возвращает объект типа, который был указан в конструкторе XmlSerializer
Event ev = (Event)ser.Deserialize(f);

Как видите немного сложнее, т.к. пришлось добавить код по сохранению/чтению типа объекта. А в остальном код идентичен сериализации в двоичном формате. Кстати в результате получиться хмл файл со следующим содержанием (конечно, если реальным типом объекта был MouseEvent):

<MouseEvent xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<XCoordinate>777</XCoordinate>
</MouseEvent>

Xml-сериализацию можно также контролировать, кстати, методы схожи.

Для полного контроля, можно реализовать интерфейс System.Xml.Serialization.IXmlSerializable:

using System.Xml.Serialization;
using System.Xml;
using System.IO;
using System.Xml.Schema;

public class MouseEvent : Event, IXmlSerializable
{
    int XCoordinate = 777;

    //этот метод зарезервирован, и не должен использоваться
    public XmlSchema GetSchema() { return null; }

    //Документ уже начат, нам осталось только вписать значения нужных нам полей
    public void WriteXml(XmlWriter writer)
    {
        writer.WriteStartElement("XCoordinate");
        writer.WriteValue(XCoordinate);
        writer.WriteEndElement();
    }

    //читаем то, что мы сохраняли
    public void ReadXml(XmlReader reader)
    {
        while (reader.Read()) // Read возвратит false если нет больше элементов для чтения
        {
            if (reader.NodeType == XmlNodeType.Element)
            {
                if (reader.Name == "XCoordinate")
                {
                    if (reader.Read() && reader.NodeType == XmlNodeType.Text)
                        XCoordinate = int.Parse(reader.Value);
                }
            }
        }
    }
}

Приятного мало, делать черновую работу, но закрытые поля по-другому нельзя сериализовать при хмл-сериализации. А еще нельзя сериализовать

  • массивы типа ArrayList
  • массивы типа List<T>

Кроме реализации интерфейса можно контролировать сериализацию путем присваивания полям, которые мы не хотим сериализовать атрибута XmlIgnoreAttribute.

Заключение

Ну, вот собственно и все, что я хотел сказать. Мы рассмотрели два способа сериализации с сохранением реального типа объекта:

-Двоичная сериализация, которая осуществляется классом System.Runtime.Serialization.Formatters.Binary.BinaryFormatter. Этот метод достаточно мощный и гибкий, кроме того, он прост и удобен в использовании. Однако то, что получается в результате доступно для чтения только самому BinaryFormatter, хотя это часто не является значимым ограничением.

-Хмл сериализация, которая осуществляется классом System.Xml.Serialization.XmlSerializer. Этот метод менее удобен, менее функционален, и единственным его преимуществом является то, что в результате получается документ в простом и понятном для восприятия и удобном для модификации формате XML.

Оба способа сериализации позволяют контролировать процесс, путем реализации соответствующего интерфейса и определения соответствующих атрибутов.

Рассмотренные методы программирования позволяют использовать все преимущества полиморфизма, на стадии сохранения и чтения данных. Теперь не нужны большие switch'и при чтении данных, чтобы сформировать структуру программы, базирующуюся на использовании полиморфизма.

Надеюсь, что не запутал вас обилием кода, однако ИМХО нет лучше способа выразить смысл использования программного кода, кроме как написать пример этого использования на языке программирования.

Удачи!!!

Богатырев Павел
23.06.2008
Редакторы: Александр Игнатьев и Юрий Таранов

Автор: PFight
Дата публикации: 23.06.2008
Число просмотров: 5562

Возврат


Copyright 2007-2009 by Alexander Ignatyev