Примечание.
- Все примеры кода в статье на языке 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.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]
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
{
int XCoordinate;
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(MouseEvent));
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 class MouseEvent : Event
{
public int XCoordinate = 777;
}
public abstract class Event
{
public void Save()
{
XmlSerializer ser = new
XmlSerializer(this.GetType());
Stream f = new StreamWriter("Test.xml", false).BaseStream;
ser.Serialize(f, this);
f.Close();
StreamWriter ft = new StreamWriter("TestType.type", false);
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;
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())
{
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
Редакторы: Александр Игнатьев и Юрий Таранов