Разработка распределенных приложений в Microsoft.NET Framework

       

Класс сериализации XmlSerializer


Класс System.Xml.Serialization.XmlSerializer реализует открытый текстовый метод сериализации, использующий XML в качестве базового формата хранения и схемы XML для спецификации документа с результатом сериализации. Данный класс используется, в частности, в веб-службах ASP.NET и при работе с очередями сообщений MSMQ. Класс XmlSerializer порождает представление объекта на языке XML, легко читаемое и модифицируемое человеком или программами. Класс XmlSerializer позволяет управлять соответствием класса и схемы XML при помощи специальных атрибутов полей классов. Благодаря открытой спецификации формата хранения, при использовании XmlSerializer возможна десериализация в тип данных, отличный от исходного, как и написание взаимодействующих компонент на основе средств разработки.

Схема XML может быть получена из описания класса, если имеется информация о всех типах объектов, которые могут содержаться во всех полях каждого класса. В частности, это означает что для класса общего вида (generic) XML-схема в общем случае не может быть получена. Однако и для конкретных классов самого описания типа недостаточно для создания схемы. Например, в поле вида IVehicle vehicleValue может содержаться объект любого класса, реализующий интерфейс IVehicle. Данная проблема решается в CLI благодаря системе атрибутов. Например, используя атрибут System.XML.Serialization.XmlAttrbute , можно перечислить все возможные типы объектов данного поля. Хотя такой подход не идеален с точки зрения объектно-ориентированного подхода, он дает возможность строго специфицировать содержимое XML-файла. Ниже приведен пример использования атрибута XmlAttrbute.

[System.XML.Serialization.XmlAttrbute(typeof(Car)), System.XML.Serialization.XmlAttrbute(typeof(Bike)), System.XML.Serialization.XmlAttrbute(typeof(LongTruck))] IVehicle vehicleValue;

Таким образом, при использовании некоторых дополнительных метаданных, для классов CLI может быть построена соответствующая им схема XML, которая будет корректной для организованного древовидно графа объектов.


Используемый классом XmlSerializer метод сериализации имеет ряд недостатков. Во-первых, он не является универсальным: сериализуемый им граф объектов не может содержать циклы. Во-вторых, он полагает, что граф объектов является деревом и записывает значение полей объекта на место их ссылки. В результате десериализации создаются столько копий объектов, сколько в соответствующем графе в него входило ребер (рис. 4.3). Поскольку на этапе построения XML-схемы нет никакой информации о каких-либо объектах, а только описания полей и свойств классов, то предполагается, что на каждый объект сериализуемого графа, отличный от его корня, существует единственная ссылка в поле какого-либо другого объекта этого же графа.


Рис. 4.3.  Применение XmlSerializer к произвольному графу объектов

Наибольшей трудностью при использовании класса XmlSerializer являются предъявляемые им требования к сериализуемым классам. В .NET Framework 2.0 XmlSerializer позволяет сериализовать публичные классы, имеющие конструктор без параметров типа public и отвечающие одному из следующих требований.

  1. Класс реализует интерфейс IXMLSerializable. В этом случае XmlSerializer просто использует при сериализации методы класса GetSchema, ReadXml, WriteXml.
  2. Класс реализует интерфейс System.Collections.IEnumerable , но не реализует ICollection и содержит публичный метод Add c единственным параметром, имеющим тип, совпадающий с типом результата свойства IEnumerator.Current метода GetEnumerator сериализуемого объекта. Такой класс сериализуется через вызовы класса IEnumerator, возвращаемого методом GetEnumerator, а его публичные поля и свойства не сериализуются.
  3. Класс реализует интерфейс System.Collections.ICollection, но не реализует IEnumerable. Для такого класса осуществляется сериализация только свойства Item и публичных полей, реализующих интерфейс ICollection. Другие публичные поля и свойства не сериализуются.
  4. Класс реализует интерфейсы ICollection и IEnumerable, имеет публичное индексированное свойство Item c целым индексом и публичное целое свойство Count.


    Тип, принимаемый методом Add, должен быть типом свойства Item или одним из его предков. Другие публичные поля и свойства не сериализуются.
  5. Класс не реализует ни один из интерфейсов IXMLSerializer, IEnumerable, ICollection и имеет атрибут System.SerializableAttribute. В этом случае будут сериализованы публичные свойства и поля класса с учетом специальных атрибутов, управляющих процессом сериализации.
Подлежащие сериализации публичные свойства должны иметь реализацию обоих методов, get и set. Кроме того, если класс не использует собственную процедуру сериализации, то он не должен иметь свойств или полей типа интерфейс или многомерных массивов, вместо них следует использовать вложенные массивы.

// недопустимо в сериализуемом автоматически классе public int[,] data = new int[2,2]; К сожалению, классы FCL, реализующие интерфейс IDictionary, не удовлетворяют этим требованием (их метод Add имеет два параметра). Поэтому главным практическим недостатком XmlSerializer является неспособность самостоятельно обрабатывать типы, реализующие интерфейс System.Collections.IDictionary, использующиеся для хранения пар ключ–значение. В частности, к ним относится популярный класс System.Collections.Hashtable. Однако можно создать собственный класс, включающий в себя поле типа Hashtable и реализующий интерфейс IXmlSerializable. XMLSerializer может обрабатывать следующие важные на практике классы из FCL:

  • System.XML.XmlNode и его наследники System.XML.XmlDocumеnt и System.XML.XmlElement, причем сериализация XML-документа является этим же XML-документом (с точностью до форматирования, не влияющего на содержимое документа);
  • класс System.Data.Dataset;
  • классы общего вида (generic classes) из System.Collections.Generic, такие как List или Queue, кроме Dictionary и SortedDictionary.
Как было указанно ранее, для большинства классов можно использовать описанные в System.Xml.Serialization атрибуты сериализации, которые позволяют установить связь между полями класса и атрибутами или элементами XML, отвечающими ему.


Также можно использовать атрибут NonSerializedAttribute для публичных полей или свойств, не подлежащих сериализации.

В качестве примера рассмотрим пример общего класса, реализующий интерфейс IXMLSerializable для сериализации коллекции пар из ключа и значения какого-либо фиксированного типа. Класс содержит два объекта сериализации – один для обработки строк, второй для типа значений.

using System.Xml; using System.Collections; using System.Xml.Serialization; public class DictionaryCollection<TValue> : DictionaryBase, IXmlSerializable { private XmlSerializer valueSerializer; private XmlSerializer keySerializer; public DictionaryCollection() { keySerializer = new XmlSerializer(typeof(String)); valueSerializer = new XmlSerializer(typeof(TValue)); } Класс имеет индексируемое свойство, отвечающее за получение значения по его ключу, и метод добавления ключа и значения в коллекцию.

public TValue this[string key] { get { return (TValue)this.Dictionary[key]; } set { this.Dictionary[key] = value; } } public void Add(String key, TValue value) { this.Dictionary.Add(key, value); } Для сериализации классом XMLSerializer класс имеет два метода – WriteXml и ReadXML.

public void WriteXml(System.Xml.XmlWriter writer) { foreach (String key in this.Dictionary.Keys) { keySerializer.Serialize(writer, key); valueSerializer.Serialize(writer, this[key]); } } public void ReadXml(System.Xml.XmlReader reader) { reader.Read(); while (reader.NodeType != XmlNodeType.EndElement) { String key = (String) keySerializer.Deserialize(reader); TValue value = (TValue) valueSerializer.Deserialize(reader); reader.MoveToContent(); Add(key, value); } } Метод GetSchema необходим для генерации XSD-файла для данного класса. Поскольку создание такой схемы для класса общего вида невозможно, метод не реализован.

public System.Xml.Schema.XmlSchema GetSchema() { return null; } } // DictionaryCollection<TValue> Ниже показан фрагмент кода, использующего данный класса для записи пар строка-число в файл.



DictionaryCollection<Int32> dictionary = new DictionaryCollection<Int32>(); XmlSerializer serializer = new XmlSerializer(typeof(DictionaryCollection<Int32>)); dictionary.Add("key1", 10); dictionary.Add("key2", 20); using (StreamWriter writer = new StreamWriter("sample.xml")) { XmlTextWriter xmlWriter = new XmlTextWriter(writer); xmlWriter.Formatting = Formatting.Indented; serializer.Serialize(xmlWriter, dictionary); }

Атрибуты XmlAttributeAttribute и XmlElementAttribute обычно применяют к скалярным полям класса. Два их опционных параметра – название атрибута или элемента XML с результатом сериализации, а также тип объекта. XmlElementAttribute может быть применен только к примитивным типам и строкам. Если тип объекта, хранящимся в поле, совпадает с указанным в типе или в атрибуте поля, то в XML-файле не будет использоваться атрибут типа xsi:type для указания типа объекта.

Атрибуты XmlArrayAttribute, XmlArrayItemAttribute используются для контейнерных классов. К ним относятся массивы, коллекции (например, ArrayList) и коллекции общего вида (например, List<T>). В этом случае атрибут XmlArray аналогичен атрибуту XmlAttribute для скалярных классов, а XmlArrayItem указывает все возможные типы элементов массива или списка и соответствующие им имена элементов XML. Для корректной обработки контейнера в атрибутах XmlArrayItem должны быть указаны все типы объектов, которые могут храниться в контейнере. Если в контейнере хранится только тип, указанный явным образом при его объявлении, то данный атрибут не обязателен. Таким образом, если в списке List<Person> persons хранятся только объекты типа Person, то атрибут XmlArrayItem не обязателен. Следующий пример служит для иллюстрации применения указанных атрибутов для сериализации.

// Файл figures.cs using System; using System.IO; using System.Xml.Serialization; using System.Collections.Generic; public abstract class GeomFigure { } Абстрактный класс фигуры имеет двух наследников, представляющих точку и прямую на плоскости.


Конструктор по умолчанию необходим для десериализации объектов.

public class GeomPoint: GeomFigure { private double xField, yField; [XmlAttribute("X")] public double X { get {return xField;} set {xField = value;} } [XmlAttribute("Y")] public double Y { get {return yField;} set {yField = value;} } public GeomPoint() { } public GeomPoint(double x, double y) { this.X = x; this.Y = y; } public override string ToString() { return String.Format("({0}, {1})", X, Y); } } public class GeomLine: GeomFigure { private GeomPoint aField, bField; public GeomPoint A { get {return aField;} set {aField = value;} } public GeomPoint B { get {return bField;} set {bField = value;} } public GeomLine() { } public GeomLine(GeomPoint a, GeomPoint b) { this.A = a; this.B = b; } public override string ToString() { return String.Format("{0} {1}", A, B); } } Листинг 4.1. В списке фигур используются атрибуты XmlArrayItem для описания всех возможных типов фигур.

public class GeomFigures { private List<GeomFigure> figuresField; [XmlArrayItem("Point", typeof(GeomPoint)), XmlArrayItem("Line", typeof(GeomLine))] public List<GeomFigure> Figures { get { return figuresField;} } public GeomFigures() { figuresField = new List<GeomFigure>(); } } public class App { public static void Main() { GeomFigures figures = new GeomFigures(); figures.Figures.Add(new GeomPoint(2, -1)); figures.Figures.Add(new GeomLine(new GeomPoint(-1, -1), new GeomPoint(2, 2))); XmlSerializer serializer = new XmlSerializer(typeof(GeomFigures)); using (StreamWriter writer = new StreamWriter("figures.xml")) { serializer.Serialize(writer, figures); }; } } Листинг 4.2. В результате выполнения этого примера будет создан следующий XML-файл.

<?xml version="1.0" encoding="utf-8"?> <GeomFigures xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Figures> <Point X="2" Y="-1" /> <Line> <A X="-1" Y="-1" /> <B X="2" Y="2" /> </Line> </Figures> </GeomFigures> Важное применение атрибутов заключается в том, что они позволяют описать соответствие класса XSD-схеме получаемого в ходе сериализации документа.


В состав .NET Framework входит утилита xsd.exe, позволяющая выполнять три основные задачи:

  • создание частичного (partial) описания класса на С# по схеме XSD;
  • создание схемы XSD по классу С#;
  • создание схемы XSD по образцу XML-файла (правильность зависит от полноты образца).
Если для указанного выше файла выполнить следующую команду, то будет создан файл schema0.xsd со схемой XML.

xsd.exe figures.exe /type:GeomFigures Сама схема будет иметь следующий вид.

<?xml version="1.0" encoding="utf-8"?> <xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="GeomFigures" nillable="true" type="GeomFigures" /> <xs:complexType name="GeomFigures"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="1" name="Figures" type="ArrayOfChoice1" /> </xs:sequence> </xs:complexType> <xs:complexType name="ArrayOfChoice1"> <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element minOccurs="1" maxOccurs="1" name="Point" nillable="true" type="GeomPoint" /> <xs:element minOccurs="1" maxOccurs="1" name="Line" nillable="true" type="GeomLine" /> </xs:choice> </xs:complexType> <xs:complexType name="GeomPoint"> <xs:complexContent mixed="false"> <xs:extension base="GeomFigure"> <xs:attribute name="X" type="xs:double" use="required" /> <xs:attribute name="Y" type="xs:double" use="required" /> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="GeomFigure" abstract="true" /> <xs:complexType name="GeomLine"> <xs:complexContent mixed="false"> <xs:extension base="GeomFigure"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="1" name="A" type="GeomPoint" /> <xs:element minOccurs="0" maxOccurs="1" name="B" type="GeomPoint" /> </xs:sequence> </xs:extension> </xs:complexContent> </xs:complexType> </xs:schema> Листинг 4.3. Следующая команда создаст по XML-схеме файл на языке C#, который при необходимости можно использовать для десериализации созданного в примере XML-файла вместо оригинального файла примера.



xsd.exe schema0.xsd /classes Таким образом, при использовании для обмена данными между компонентами класса XmlSerializer можно как создать схему по сериализуемому классу и представить ее в качестве спецификации передаваемых данных, так и сгенерировать на языке C# код описания публичных свойств класса из XSD-схемы. На рисунке 4.4 показана схема применения XML-сериализатора в распределенных системах. В зависимости от применения класса XMLSerializer схема XML может передаваться как отдельный документ, специфицирующий формат обмена сообщениями между компонентами, или входить в описание интерфейса программной компоненты на языке WSDL.


Рис. 4.4.  Использование схем XML для сериализации объектов

Особенностью класса XMLSerializer является то, что в ходе работы он создает сборки с кодом сериализации для каждого обрабатываемого класса при вызове его конструктора. Например, в описанном ранее примере в памяти будет создана сборка с именем GeomFigures.XmlSerializers, что приводит к определенной однократной задержке.

Если такая задержка нежелательна (например, программа не совершает повторяющейся сериализации одного и того же класса, но желает при этом осуществлять операции максимально быстро), то при помощи утилиты sgen.exe можно заранее создать такие сборки и затем подключить их к проекту.

Резюмируя краткое описание XMLSerializer, следует отметить, что несмотря на отдельные сложности его применения к некоторым классам, его использование позволяет создать открытое взаимодействия удаленных компонент со спецификацией сериализуемых классов в виде схемы XSD. При использовании XMLSerializer можно также рекомендовать вместо сложных структур данных использовать классы XmlDocument или Dataset, особенно если такие структуры включают неподдерживаемые XML-сериализацией классы. Сериализуемый тип может быть классом общего вида, но невозможность создания для таких классов схемы XML-документа ограничивают их применение. Особенностью класса XmlSerializer является жесткая привязка к десериализуемому типу данных, обычно передаваемому ему в качестве аргумента конструктора.


Содержание раздела