Разработка распределенных приложений в 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 и отвечающие одному из следующих требований.

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


Тип, принимаемый методом Add, должен быть типом свойства Item или одним из его предков. Другие публичные поля и свойства не сериализуются. Класс не реализует ни один из интерфейсов 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 является жесткая привязка к десериализуемому типу данных, обычно передаваемому ему в качестве аргумента конструктора.


Классы сериализации SoapFormatter и BinaryFormatter


Класс сериализации System.Runtime.Serialization.Formatters.Soap.SoapFormatter используется исключительно в среде .NET Remoting, а класс System.Runtime.Serialization.Formatters.Binary.BinaryFormatter может также использоваться в среде MSMQ вместо XMLSerializer. Оба класса форматирования по приведенной классификации являются универсальными. Класс форматирования BinaryFormatter реализует двоичный закрытый метод сериализации, класс SoapFormatter – текстовый и открытый, основанный на спецификации кодирования SOAP-RPC (пространство имен http://schemas.xmlsoap.org/soap/encoding/).

При разработке .NET Framework 2.0 разработчики по некоторым данным собирались придать классу SoapFormatter статус устаревшего. Класс SoapFormatter не поддерживает одно из важных нововведений – параметризированные типы данных (generic types).

Оба указанных класса в простейшем случае при сериализации сохраняют все поля класса (но не его свойства), вне зависимости от их видимости. Поля, имеющие атрибут System.NonSerializeAttribute, игнорируются. Класс должен иметь атрибут System.SerializableAttribute. В ходе сериализации класса форматирования используют методы класса System.Runtime.Serialization.FormatterServices. Сериализуемый класс должен содержать конструктор без параметров, который вызывается при создании нового объекта в ходе десериализации.

Если же обрабатываемый класс реализует интерфейс ISerializable, то он сериализуется вызовом метода GetObjectData(SerializationInfo info, StreamingContext context) этого интерфейса, внутри которого обычно так же вызываются методы FormatterServices. Десериализация таких классов осуществляется вызовом конструктора ISerializable(SerializationInfo info, StreamingContext context), заполняющего поля объекта значениями из info.

О завершении своей десериализации объект может получить уведомление, реализовав интерфейс System.Runtime.Serialization.IDeserializationCallback с единственным методом OnDeserialization.

Полученный таким образом на первом шаге сериализации объект класса SerializationInfо содержит имена и значения сериализуемых полей.
Рассматриваемые классы форматирования, реализующие интерфейс IFormatter, преобразуют эти имена в некоторый вид, передаваемый между доменами приложения через потоки ввода-вывода.

Рассмотрим пример создания класса с интерфейсом ISerializable и собственным механизмом сериализации.

using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters; using System.Runtime.Serialization.Formatters.Binary; using System.Reflection; [Serializable] public class Person : ISerializable { public String name; public Person() { } Метод GetObjectData используется на первом шаге сериализации класса. В ходе его работы в объект класса SerializationInfo добавляется информация о полях класса, подлежащих сериализации. Для получения метаданных о полях класса используется статический метод GetSerializableMembers класса FormatterServices.

public void GetObjectData(SerializationInfo info, StreamingContext context) { Type thisType = this.GetType(); MemberInfo[] serializableMembers = FormatterServices.GetSerializableMembers(thisType, context); foreach (MemberInfo serializableMember in serializableMembers) { // Не обрабатывать поля с аттрибутом NonSerializedAttribute if (!(Attribute.IsDefined(serializableMember, typeof(NonSerializedAttribute)))) { info.AddValue(serializableMember.Name, ((FieldInfo)serializableMember).GetValue(this)); } } } Для проведения десериализации класс содержит конструктор специального вида, заполняющий поля класса значениями из объекта класса SerializationInfo.

protected Person(SerializationInfo info, StreamingContext context) { Type thisType = this.GetType(); MemberInfo[] serializableMembers = FormatterServices.GetSerializableMembers(thisType, context); foreach (MemberInfo serializableMember in serializableMembers) { FieldInfo fieldInformation = (FieldInfo)serializableMember; if (!(Attribute.IsDefined(serializableMember, typeof(NonSerializedAttribute)))) { fieldInformation.SetValue(this, info.GetValue(fieldInformation.Name, fieldInformation.FieldType)); } } } } // Person Ниже приведен пример использования созданного класса Person.



public class SampleApp { public static void Main() { using (Stream stream = new MemoryStream()) { IFormatter formatter = new BinaryFormatter(); Person person = new Person(); person.name = "Иван"; Console.WriteLine("Сохранено: {0}", person.name); formatter.Serialize(stream, person); stream.Position = 0; Person personRestored = (Person) formatter.Deserialize(stream); Console.WriteLine("Восстановлено: {0}", personRestored.name); } } } Классы форматирования имеют механизм, позволяющий изменить процедуры сериализации и десериализации для объектов некоторого класса и его потомков. Это необходимо, в частности, при использовании удаленных объектов, которые маршализируются по ссылке и не пересекают границы домена приложения. Такие объекты находятся на сервере, а на стороне клиента для их использования должен быть создан некоторый посредник, реализующий весь интерфейс удаленного объекта, включая доступ к его полям и свойствам. Для реализации маршализации по ссылке к объекту форматирования через поле SurrogateSelector можно присоединить класс, реализующий интерфейс System.Runtime.Serialization.ISurrogateSelector. Он должен связывать тип удаленного объекта со специальной процедурой его сериализации и десериализации. Использование этого механизма в .NET Remoting приводит к тому, что наследники класса MarshalByRefObject не покидают своего домена приложения. При использовании же BinaryFormatter в среде MSMQ наследники MarshalByRefObject сериализуется обычным образом, поскольку использующий объекты форматирования класс BinarryMessageFormatter не создает связанный с типом MarshalByRefObject объект класса SurrogateSelector.

Использование класса BinaryFormatter является наиболее эффективным и универсальным, но и самым закрытым способом сериализации. Этот класс позволяет передавать между доменами приложения произвольный граф объектов, но при его использовании распределенная система теряет свойство открытости. В случае применения этого класса взаимодействующие компоненты могут быть созданы только на платформе CLI, причем обоим сторонам необходимо иметь сборку с сериализуемым типом.


При использовании в качестве параметров типов из стандартной библиотеки или использующих их классов желательно, чтобы обе стороны были реализованы на одной версии CLI. Поэтому для передачи сложных типов лучше всего использовать XML. Однако, стандартный класс System.Xml.XmlDocument не может быть сериализован классами BinaryFormatter и SoapFormatter, поскольку данный класс по неясным причинам не имеет атрибута Serializable. Для сериализации объектов класса XmlDocument проще всего преобразовать его в строку, и затем сериализовать ее. Можно так же создать наследника XmlDocument, который будет реализовывать интерфейс ISerializable.

Ниже приводится пример вспомогательного класса с двумя статическими методами, преобразующими объект класса XmlDocument в строку и наоборот. Поскольку метод XmlDocument.ToString() против ожиданий не возвращает текст XML-документа и у него нет метода, обратного LoadXml, то следует использовать класс StringWriter.

// SevaXmlUtils.cs using System; using System.IO; using System.Xml; namespace Seva.Xml { public static class XmlUtils { public static String XmlToString(XmlDocument xml) { StringWriter xmlLine = new StringWriter(); xml.Save(xmlLine); return xmlLine.ToString(); } public static XmlDocument XmlFromString(String xmlLine) { XmlDocument xml = new XmlDocument(); xml.LoadXml(xmlLine); return xml; } } }

Методы сериализации в NET Framework


Microsoft .NET Framework 2.0 поддерживает четыре технологии удаленного взаимодействия – Message Queuing, Enterprise Services, Remoting, Web services. Какая бы технология не была выбрана, в любом случае передача данных между удаленными компонентами является одним из основных моментов удаленного взаимодействия. Данные могу передаваться как сообщение в очереди (MSMQ) или как параметры удаленного вызова и результат вызываемой функции (остальные технологии). В любом случае, для реализации распределенной системы необходимо определить процедуры сериализации и десериализации для передаваемых классов.

Классы, производящие сериализацию и десериализацию в .NET Framework, называются классами форматирования (formatters). Не считая служащего для обеспечения совместимости ActiveXMessageFormatter, в .NET Framework выделяется три различных независимых класса форматирования: XmlSerializer, SoapFormatter, BinaryFormatter. Они используются как для записи и чтения объектов из потоков ввода-вывода, так и для построения распределенных систем:

технология веб служб ASP.NET использует XmlSerializer; технология Remoting использует SoapFormatter, BinaryFormatter или созданный пользователем класс; при работе с сообщениями MSMQ используется XmlSerializer (через XMLMessageFormatter), или BinaryFormatter (через BinaryMessageFormatter), или созданный пользователем класс; технология Enterprise Services основана на Remoting и использует BinaryFormatter.

Можно провести классификацию возможных методов сериализации по следующим основным признакам.

1. Классификация по виду обрабатываемого графа:

универсальные методы: граф произвольного вида с циклами;произвольный ациклический граф;дерево.

2. Классификация по формату хранения информации в хранилище:

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

3. Классификация по спецификации формата данных, полученного в результате сериализации:

закрытые методы: спецификация задается только при помощи интерфейсов всех классов, объекты которых образуют граф; в этом случае обе удаленные компоненты должны быть созданы на одной языковой платформе, причем обе стороны должны иметь как минимум описание интерфейса сериализуемых классов; открытые методы: спецификация может быть задана в виде общепринятого формата, например схемы XSD.

По данной классификации XmlSerializer реализует открытый текстовый неуниверсальный метод, BinaryFormatter – закрытый двоичный универсальный метод, SoapFormatter – текстовый открытый метод. Дополнительной особенностью каждого из классов, за исключением BinaryFormatter, является ограничения на классы, которые подлежат сериализации. Данные ограничения будут рассмотрены далее.



Сериализация графа объектов


В отличие от приложений на неуправляемом коде, приложения .NET Framework не обязательно выполняются в виде отдельных процессов, а могут существовать в пределах одного процесса операционной системы в своих собственных областях, называемых доменами приложения. Такие области можно рассматривать как некоторые логические процессы виртуальной машины CLR. Использование управляемого кода позволяет при этом гарантировать изоляцию приложений в пределах своих областей. При передаче между доменами приложений некоторого объекта для его класса должна быть определена процедура сериализации, которая позволяет сохранить состояние объекта в некотором внешнем хранилище (например, в файле, или в сообщении транспортного протокола) при помощи потоков ввода-вывода, и процедура десериализации, создающая копию объекта по сохраненному состоянию (рис. 4.1). Следует отметить, что в общем случае это могут быть объекты разных классов, и даже созданные в разных системах разработки приложений.


Рис. 4.1.  Сериализация и десериализация объекта

Задача сериализации объекта, включающего только поля из элементарных типов значений (наследников класса System.ValueType) и строк, не представляет принципиальных трудностей. Для такого объекта в ходе сериализации в поток записываются сами значения всех полей объекта. Однако в общем случае объект содержит ссылки на другие объекты, которые, в свою очередь, могут ссылаться друг на друга, образуя так называемый граф объектов (object graph). Сами ссылки не могут быть сохранены в потоке ввода-вывода, поэтому основной вопрос сериализации – это способ замены ссылок.

Граф объектов – ориентированный граф G = < V, E > , в котором вершины – это объекты (множество V), а ребра направлены от объектов, содержащие ссылки, на ссылаемые объекты (рис. 4.2).

public class SampleClass { public SampleClass fieldA = null; public SampleClass fieldB = null; } ... SampleClass root = new SampleClass(); SampleClass child1 = new SampleClass(); SampleClass child2 = new SampleClass(); root.fieldA = child1; root.fieldB = child2; child1.fieldA = child2; ...




Рис. 4.2.  Граф объектов

Наличие ссылки на объект B в объекте A есть принадлежность пары < A, B > множеству E. При рассмотрении графа все поля объектов, относящиеся к простым типам значениям и строкам, можно исключить из рассмотрения в силу тривиальности их сериализации. Хотя формально строки являются ссылочными типами, особенность реализации строк в CLI не дает возможность изменить уже созданную строку. Эта особенность позволяет тривиально обрабатывать строки при сериализации, сохраняя в потоке их содержимое.

Существует частный случай, когда сериализация выполняется тривиально. Для этого граф должен иметь вид так называемого ориентированного дерева. В каждую вершину дерева, отличную от корня, направлено единственное ребро, и существует единственная вершина, в которую не ведет ни одного ребра, называемая корнем. В таком случае сериализация может вестись от корня методом в глубину, а сериализация полей-ссылок выполняется аналогично сериализации полей значений – при обнаружении ссылки на объект вместо нее в хранилище помещаются значения полей ссылаемого объекта и некоторая дополнительная информация о типе объекта. Например, если существуют ребра < A, B1>, ... < A, Bn > , функция сериализации S для объекта A может быть определена как S(A) = < V(A), <S(B1),  ... S(Bn)>> , где функция V – значение полей значений и строк указанного объекта.

Проблема сериализации графа объектов связана с тем, что ссылка на один и тот же объект может быть значением поля у разных объектов (существуют ребра < A1, B> и < A2, B > ). Возможно также наличие циклов вида A 
 ...  
 A.

В этом случае в ходе сериализации объектам должны быть поставлены в соответствие некоторые идентификаторы, и в хранилище отдельно сохраняется список объектов, отмеченный идентификаторами, а при сериализации вместо ссылок записываются идентификаторы ссылаемых объектов. Если A1, ... An – все вершины графа, то его образом после сериализации будет множество {<idA1, S(A1)>, ... <idAn, S(An)>} , где S(A) определена как S(A) = < V(A), <idB1, ...


idBn> > , где B1, ... Bn – объекты, ссылки на которые непосредственно содержатся в объекте A. В программе роль идентификатора объекта выполняет его адрес, но вместо него обычно удобнее назначить некоторые идентификаторы в процедуре сериализации для более легкого чтения человеком полученного образа. В ходе сериализации нужно ввести список адресов уже записанных объектов, как для ведения списка идентификаторов, так и для обнаружения возможных циклов при обходе графа методом в глубину.

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


Выводы по использованию классов сериализации


Все классы сериализации библиотеки .NET Framework имеют свои особенности и ограничения, что может вызвать значительные изменения в программном коде при переходе с одной промежуточной среды на другую. Один из способов борьбы с этой проблемой состоит в отказе от сериализации нетривиальных классов (содержащих что-либо, кроме примитивных типов-значений и строк), и особенно сложных списочных структур. Вместо них, вероятно, следует использовать наборы данных (класс System.Data.Dataset) или документы XML (класс System.Xml.XMLDocument). Хотя такой способ может являться не совсем удобным для разработчика, он дает залог создания независимого от класса форматирования программного кода.