Сериализация — процесс преобразования объекта или группы связанных объектов в поток байт или набор XML узлов для сохранения (в базу данных, файл и т.д.) или передачи (например, по сети). Десериализация — обратный процесс — восстановление объекта из потока байт или XML узлов. Сериализация обычно используется для передачи объектов по сети или за границы приложения, для сохранения объектов внутри файлов или базы данных, а также для глубокого копирования объектов.
Классы для сериализации располагаются в двух пространствах имен: System.Runtime.Serialization
и System.Xml.Serialization
.
Механизмы сериализации
Для сериализации в .NET доступно 4 механизма:
- сериализатор контрактов данных
- двоичный сериализатор
- XML сериализатор (
XmlSerializer
) - интерфейс
IXmlSerializable
Первые три являются готовыми механизмами для сериализации и выполняют всю работу самостоятельно, интерфейс IXmlSerializable
предполагает реализацию механизма сериализации самостоятельно.
Наличие трех разных механизмов сериализации является исторически сложившимся.
Сериализатор контрактов данных является самым новым и универсальным. Он может быть использован при обмене информацией через протоколы обмена сообщениями. Он также обеспечивает хорошую переносимость версий и дает возможность десериализовать данные полученные из более ранней или более поздней версии типа, за счет того что устраняет связь между низкоуровневыми деталями объектов и структурой сериализованных данных. Сериализатор контрактов данных может быть использован в большинстве задач вместо двоичного и XML сериализатора.
Двоичный сериализатор прост в применении, хорошо автоматизирован и поддерживается повсеместно в .NET. Двоичная сериализация используется инфраструктурой Remoting, в т.ч. при взаимодействии между двумя доменами приложений в одном и том же процессе. Двоичный сериализатор работает быстрее чем сериализатор контрактов данных, но так дает худшую переносимость, поскольку тесно связывает внутреннюю структуру типа с форматом сериализованных данных. Он также не может быть использован для получения XML.
XmlSerializer
может генерировать только XML, и по сравнению с другими механизмами он менее функционален при сериализации сложных групп объектов. Однако во взаимодействии с XML он дает наибольшую функциональность, а также обеспечивает хорошую переносимость версий.
Реализация интерфейса IXmlSerializable
предполагает самостоятельное выполнение сериализации с помощью XmlReader
и XmlWriter
.
Вывод сериализатора контрактов данных и двоичного сериализатора оформляется с помощью подключаемого форматера. Форматер приводит форму финального представления в соответствие с конкретной средой или контекстом сериализации. Доступно два форматера: форматер XML и двоичный форматер. XML форматер используется в контексет чтения/записи XML, тектовых файлов и потоков, обмена сообщениями SOAP. Двоичный форматер используется в контексте произвольного потока байт. Двоичный вывод по размерам обычно меньше чем XML, иногда значительно. Теоретически механизм сериалзиции не связан с форматером, но на практике сериализатор контрактов данных использует XML форматер, а двоичный сериализатор — двоичный форматер.
Сериализатор контрактов данных
Использование сериализатора контрактов данных предполагает следующие три шага
- выбрать класс для использования:
DataContractSerializer
илиNetDataContractSerializer
- добавить сериализуемым типам и членам атрибуты
[DataContract]
и[DataMember]
(соответственно) - создать экземпляр сериализатора и вызвать его метод
WriteObject
илиReadObject
Существует два сериализатора контрактов данных:
DataContractSerializer
— обеспечивает слабую привязку типов .NET к типам контрактов данных. Может генерировать совместимый со стандартами XML код. Требует предварительной явной регистрации сериализуемых производных типов, чтобы иметь возможность сопоставлять имена контрактов данных с именами типов .NETNetDataContractSerializer
— характеризуется тесной привязкой типов .NET к типам контрактов данных, не требует явной регистрации сериализуемых производных типов, т.к. самостоятельно записывает полные имена типов и сборок сериализуемых типов
После выбора сериализатора необходимо всем сериализуемым типам добавить атрибут[DataContract]
, а их членам, которые необходимо включить в сериализацию, — атрибуты [DataMember]
:
1 2 3 4 5 6 7 8 | namespace SerialTest { [DataContract] public class Person { [DataMember] public string Name; [DataMember] public int Age; } } |
После этого можно явно сериализовать и десериализовать объекты, создавая экземпляр DataContractSerializer
или NetDataContractSerializer
и вызывая метод WriteObject
или ReadObject
:
1 2 3 4 5 6 7 8 | Person p = new Person { Name = "Stacey", Age = 30 }; var ds = new DataContractSerializer (typeof (Person)); using (Stream s = File.Create ("person.xml")) ds.WriteObject (s, p); // Сериализация Person p2; using (Stream s = File.OpenRead ("person.xml")) p2 = (Person) ds.ReadObject (s); // Десериализация Console.WriteLine (p2.Name + " " + p2.Age); // Stacey 30 |
Конструктору DataContractSerializer
необходимо передать тип корневого объекта — сериализуемый объект, который в XML дереве будет корневым элементом. Конструктор NetDataContractSerializer
этого не требует. В остальном их применение аналогично.
Оба типа сериализаторов по умолчанию принимаю форматер XML, поэтому с ними можно использовать XmlWriter
:
1 2 3 4 5 6 7 8 | Person p = new Person { Name = "Stacey", Age = 30 }; var ds = new DataContractSerializer (typeof (Person)); // Добавить в вывод отступы: XmlWriterSettings settings = new XmlWriterSettings() { Indent = true }; using (XmlWriter w = XmlWriter.Create ("person.xml", settings)) ds.WriteObject (w, p); System.Diagnostics.Process.Start ("person.xml"); |
Имена XML элементов в выводе соответствуют именам контрактов данных, которые по умолчанию совпадают с именами типов в .NET, однако это можно переопределить с помощью атрибутов, задав альтернативное имя для элемента:
1 2 | [DataContract (Name="Candidate")] public class Person { ... } |
Также можно изменить пространство имен по умолчанию для корневого элемента:
1 2 | [DataContract (Namespace="http://somedomain.com/contacts")] public class Person { ... } |
Можно также переопределить имена членов данных:
1 2 3 4 5 6 | [DataContract (Name="Candidate", Namespace="http://oreilly.com/nutshell")] public class Person { [DataMember (Name="FirstName")] public string Name; [DataMember (Name="ClaimedAge")] public int Age; } |
Атрибут [DataMember]
может быть применен к частным и публичным полям и свойствам следующих типов данных:
- любой примитивный тип
DateTime
,TimeSpan
,Guid
,Uri
илиEnum
- типы, допускающие значение
null
вышеуказанных типов byte[]
(сериализуется в XML с применением кодировки Base64- любой тип с атрибутом
[DataContract]
- любой тип
IEnumerable
- любой тип с атрибутом
[Serializable]
или реализующий интерфейсISerializable
- любой тип реализующий интерфейс
IXmlSerializable
Можно использовать двоичный форматер:
1 2 3 4 5 6 7 8 9 | Person p = new Person { Name = "Stacey", Age = 30 }; var ds = new DataContractSerializer (typeof (Person)); var s = new MemoryStream(); using (XmlDictionaryWriter w = XmlDictionaryWriter.CreateBinaryWriter (s)) ds.WriteObject (w, p); var s2 = new MemoryStream (s.ToArray()); Person p2; using (XmlDictionaryReader r = XmlDictionaryReader.CreateBinaryReader (s2, XmlDictionaryReaderQuotas.Max)) p2 = (Person) ds.ReadObject (r); |
Сериализация производных типов
При сериализации производных типов если используется NetDataContractSerializer
никаких дополнительных действий не требуется, необходимо только снабдить производный класс атрибутом[DataContract]
. Сериализатор будет записывать полностью определенные имена типов:
1 2 3 | <Person ... z:Type="SerialTest.Person" z:Assembly="SerialTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> |
В случае с DataContractSerializer
сериализатору необходимо сообщить обо всех производных типах, которые он может сериализовать. Если этого не сделать при десерриализации возникнет ошибка, т.к. сериализатор не сможет узнать в какой именно тип следует преобразовать элемент:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Базовый класс: [DataContract] public class Person { [DataMember] public string Name; [DataMember] public int Age; } // Производные классы: [DataContract] public class Student : Person { } [DataContract] public class Teacher : Person { } // Метод выполняющий глубокое клонирование с применением сериализации: static Person DeepClone (Person p) { var ds = new DataContractSerializer (typeof (Person)); MemoryStream stream = new MemoryStream(); ds.WriteObject (stream, p); stream.Position = 0; return (Person) ds.ReadObject (stream); } // Применение метода: Person person = new Person { Name = "Stacey", Age = 30 }; Student student = new Student { Name = "Stacey", Age = 30 }; Teacher teacher = new Teacher { Name = "Stacey", Age = 30 }; Person p2 = DeepClone (person); // OK Student s2 = (Student) DeepClone (student); // SerializationException Teacher t2 = (Teacher) DeepClone (teacher); // SerializationException |
Сообщить сериализатору о производных типах можно при создании экземпляра DataContractSerializer
, либо в самом типе с помощью атрибута[KnownType]
:
1 2 3 4 | var ds = new DataContractSerializer (typeof (Person), new Type[] { typeof (Student), typeof (Teacher) } ); [DataContract, KnownType (typeof (Student)), KnownType (typeof (Teacher))] public class Person |
Сериализованный объект Student
будет выглядеть так:
1 2 3 4 5 | <Person xmlns="..." xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:type="Student" > ... <Person> |
Поскольку в качестве корневого типа указан Person
, корневой элемент имеет его имя, а действительный подкласс описан отдельно в атрибуте type
.
Объектные ссылки
Ссылки на другие объекты также сериализуются. Напрмер:
1 2 3 4 5 6 7 8 9 10 | [DataContract] public class Person { [DataMember] public string Name; [DataMember] public int Age; [DataMember] public Address HomeAddress; } [DataContract] public class Address { [DataMember] public string Street, Postcode; } |
Будет сериализовано в:
1 2 3 4 5 6 7 8 | <Person...> <Age>...</Age> <HomeAddress> <Street>...</Street> <Postcode>...</Postcode> </HomeAddress> <Name>...</Name> </Person> |
Класс NetDataContractSerializer
всегда сохраняет эквивалентность ссылок на объекты. Класс DataContractSerializer
по умолчанию этого не делает. Это означает, что если на один и тот же объект имеются ссылки в двух разных местах, DataContractSerializer
запишет его дважды. Например, для такого объекта:
1 2 3 4 5 6 7 | [DataContract] public class Person { [DataMember] public Address HomeAddress, WorkAddress; } Person p = new Person { Name = "Stacey", Age = 30 }; p.HomeAddress = new Address { Street = "Odo St", Postcode = "6020" }; p.WorkAddress = p.HomeAddress; |
XML будет содержать тот же самый адрес дважды:
1 2 3 4 5 6 7 8 9 10 | ... <HomeAddress> <Postcode>6020</Postcode> <Street>Odo St</Street> </HomeAddress> ... <WorkAddress> <Postcode>6020</Postcode> <Street>Odo St</Street> </WorkAddress> |
При последующей десериализации WorkAddress
и HomeAddress
будут преобразованы в два разных объекта. Преимуществом такого подходя является простота и совместимость XML, а недостатком — большой размер XML, потеря ссылочной целостности и невозможность справиться с циклическими ссылками.
Чтобы сохранить ссылочную эквивалентность, нужно в конструктор DataContractSerializer
для аргумента preserveObjectReferen
передать true
:
1 2 | var ds = new DataContractSerializer (typeof (Person), null, 1000, false, true, null); |
При этом третьим аргументом необходимо передать максимальное количество объектных ссылок, которые сериализатор должен отслеживать (при привышении этого количество будет сгенерировано исключение). XML при сохранении ссылочной эквивалентности будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 | <Person xmlns="http://schemas.datacontract.org/2004/07/SerialTest" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" z:Id="1"> <Age>30</Age> <HomeAddress z:Id="2"> <Postcode z:Id="3">6020</Postcode> <Street z:Id="4">Odo St</Street> </HomeAddress> <Name z:Id="5">Stacey</Name> <WorkAddress z:Ref="2" i:nil="true" /> </Person> |
Совместимость версий
Можно добавлять и удалять члены в сериализуемые типы, не нарушая при этом прямой и обратной совместимости. Сериализаторы всегда пропускают при сериализации члены, для которых не установлен атрибут [DataMember]
, а при десериализации не генерируют исключений, если для члена, снабженного атрибутом [DataMember]
, нет сериализованных данных. Нераспознанные данные, присутствующие в потоке сериализации, но отсутствующие у десериализуемого типа, также молча пропускаются. Однако если тип реализует интерфейс IExtensibleDataObject
, нераспознанные данные пропускаться не будут, а будут сохраняться в объект ExtensionDataObject
, который может быть получен с помощью свойства ExtensionData
:
1 2 3 4 5 | [DataContract] public class Person : IExtensibleDataObject { [DataMember] public string Name; [DataMember] public int Age; ExtensionDataObject IExtensibleDataObject.ExtensionData { get; set; } } |
Если член является важным для объекта можно потребовать его присутствия с помощью IsRequired
:
1 | [DataMember (IsRequired=true)] public int ID; |
Если такой член отсутствует, при десериализации будет сгенерировано исключение.
Упорядочение членов
Порядок следования членов при сериализации и десериализации играет определенное значение: при десериализации неправильно упорядоченные члены будут пропущены.
Упорядочение осуществляется при сериализации по следующим правилам:
- сначала сериализуются члены базового класса, потом производного
- для членов с установленным аргументом
Order
учитывается его значение (от меньшего к большему) - в последнюю очередь учитывается алфавитный порядок имен членов
Таким образом поле Age
будет предшествовать полю Name
, если только не поменять их порядок с помощью аргумента Order
:
1 2 3 4 5 | [DataContract] public class Person { [DataMember (Order=0)] public string Name; [DataMember (Order=1)] public int Age; } |
Пустые значения и null
Члены типа, значениями которых является null
или пустые значения, при сериализации по умолчанию все равно записываются с пустым значением:
1 2 3 4 | <Person xmlns="..." xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <Name i:nil="true" /> </Person> |
Однако это не всегда целесообразно, поскольку ведет к бесполезному расходу пространства особенно для типов с множеством пустых полей и свойств. Поэтому сериализатору можно сообщить о необходимости пропускать пустые поля и свойства. Сделать это можно с помощью аргумента EmitDefaultValue
:
1 2 3 4 5 | [DataContract] public class Person { [DataMember (EmitDefaultValue=false)] public string Name; [DataMember (EmitDefaultValue=false)] public int Age; } |
В результате Name
будет пропущен, если его значение равно null
, а Age
будет пропущен, если его значение равно 0
.
Сериализация коллекций
Сериализаторы контрактов данных могут сериализовать и десериализовать любую коллекцию:
1 2 3 4 5 6 7 8 9 | [DataContract] public class Person { ... [DataMember] public List<Address> Addresses; } [DataContract] public class Address { [DataMember] public string Street, Postcode; } |
Вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <Person ...> ... <Addresses> <Address> <Postcode>6020</Postcode> <Street>Odo St</Street> </Address> <Address> <Postcode>6152</Postcode> <Street>Comer St</Street> </Address> </Addresses> ... </Person> |
При этом сериализатор не кодирует информацию о конкретном типе коллекции. Это позволяет при десериализации изменить тип коллекции без генерации ошибок.
При сериализации кастомного типа коллекции можно настроить имя XML узла для каждого элемента коллекции. Для этого используется атрибут[CollectionDataContract]
:
1 2 3 4 5 6 7 | [CollectionDataContract (ItemName="Residence")] public class AddressList : Collection<Address> { } [DataContract] public class Person { ... [DataMember] public AddressList Addresses; } |
Вывод будет таким:
1 2 3 4 5 6 7 | ... <Addresses> <Residence> <Postcode>6020</Postcode <Street>Odo St</Street> </Residence> ... |
Атрибут [CollectionDataContract]
также позволяет задать аргументы Namespace
и Name
. Аргумент Name
не используется, когда коллекция сериализуется как свойство другого объекта, но применяется если коллекция сериализуется как корневой объект.
Атрибут [CollectionDataContract]
может также использоваться для управления сериализацией словарей:
1 2 3 4 5 6 7 8 9 | [CollectionDataContract (ItemName="Entry", KeyName="Kind", ValueName="Number")] public class PhoneNumberList : Dictionary <string, string> { } [DataContract] public class Person { ... [DataMember] public PhoneNumberList PhoneNumbers; } |
Вывод:
1 2 3 4 5 6 7 8 9 10 11 | ... <PhoneNumbers> <Entry> <Kind>Home</Kind> <Number>08 1234 5678</Number> </Entry> <Entry> <Kind>Mobile</Kind> <Number>040 8765 4321</Number> </Entry> </PhoneNumbers> |
Хуки сериализации
Непосредственно до и после сериализации или десерализации можно выполнить специальный метод. Задать такой метод можно с помощью следующих атрибутов:
[OnSerializing]
— указывает метод для вызова перед сериализацией[OnSerialized]
— указывает метод для вызова после сериализации[OnDeserializing]
— указывает метод для вызова перед десериализацией[OnDeserialized]
— указывает метод для вызова после десериализации
Такой метод должен принимать единственный параметр, имеющий тип StreamingContext
, но необходим он только для согласованности с механизмом двоичной сериализации и не используется сериализаторами контрактов данных.
Поскольку сериализаторы не выполняют конструкторы и инициализаторы при десериализации, данные методы могут использоваться как конструкторы или инициализаторы полей при десериализации, чтобы присвоить значения членам, исключенным из сериализации, поскольку без этого исключенные из сериализации члены получат при десериализации дефолтные значения для своих типов, а не те значения, которые им присваиваются в конструкторах или инициализаторах.
Методы могут быть как публичными так и частными. Производные типы могут определять свои хуки и они также будт вызваны.
Поддержка механизмов двоичной сериализации
Сериализаторы контрактов данных также может сериализовать типы помеченные атрибутом [Serializable]
и реализующие интерфейс ISerializable
. При этом не происходит переключения на двоичную сериализацию, но многие механизмы работают ка при двоичной сериализации: учитываются атрибуты [NonSerialized]
, выполняются методы GetObjectData
и конструкторы десериализации.
Не допускается одновременно использовать атрибуты контрактов данных и атрибуты двоичной сериализации.
Двоичный сериализатор
Сделать тип поддерживающим двоичную сериализацию можно двумя путями:
- добавить типу атрибут
[Serializable]
- реализовать в типе интерфейс
ISerializable
Добавление атрибута проще, но реализация интерфейса дает больше возможностей.
Атрибут [Serializable]
Сделать тип сериализуемым можно с помощью единственного атрибута:
1 2 3 4 5 | [Serializable] public sealed class Person { public string Name; public int Age; } |
Атрибут [Serializable]
указывает сериализатору на необходимость включать все поля данного типа, как публичные так и частные, но не включать свойства.
Каждое поле должно иметь сериализуемый тип, т.е. тип также помеченный атрибутом [Serializable]
, либо реализующий интерфейс ISerializable
. Примитивные типы и множество других типов .NET являются сериализуемыми.
Атрибут Serializable
не наследуется, поэтому производные классы не являются автоматически сериализуемыми, если их не пометить специально атрибутом Serializable
.
Свойства типа не сериализуются, но сериализуются лежащие в основе их автоматически создаваемые поля (к сожалению эти поля могут переименовываться при перекомпиляции).
Выполнение двоичной сериализации
Чтобы выполнить двоичную сериализацию необходимо создать объект форматера и вызвать его метод Serialize
. Для двоичной сериализации предусмотрено два форматера:
BinaryFormatter
— более эффективный, генерирует небольшой вывод за меньшее время. Определен в пространстве именSystem.Runtime.Serialization.Formatters.Binary
SoapFormatter
— поддерживает базовый обмен сообщениями, менее функционален, не поддерживает сериализацию обобщенных типов и фильтрацию посторонних данных. Определен в пространстве именSystem.Runtime.Serialization.Formatters.Soap
Оба форматера используются одинаково:
1 2 3 4 | Person p = new Person() { Name = "George", Age = 25 }; IFormatter formatter = new BinaryFormatter(); using (FileStream s = File.Create ("serialized.bin")) formatter.Serialize (s, p); |
Метод Deserialize
восстанавливает объект:
1 2 3 4 5 | using (FileStream s = File.OpenRead ("serialized.bin")) { Person p2 = (Person) formatter.Deserialize (s); Console.WriteLine (p2.Name + " " + p.Age); // George 25 } |
При воссоздании объектов десериализатор пропускает все конструкторы.
Сериализованные данные включают полные сведения о типе и сборке, поэтому если при десереиализации попытаться привести тип к совместимому типу, возникнет ошибка. Десериализатор полностью восстанавливает объектные ссылки в их исходное состояние.
Двоичные сериализаторы поддерживают также атрибуты [OnSerializing]
,[OnSerialized]
, [OnDeserializing]
и [OnDeserialized]
. Их применение не отличается от сериализаторов контрактов данных.
Атрибут [NonSerialized]
В отличие от сериализаторов контрактов данных, которые требую помечать атрибутами все сериализуемые поля, двоичный сериализатор этого не требует и по умолчанию включает все поля сериализуемого объекта. Исключить отдельные поля из сериализации можно отметив их атрибутом [NonSerialized]
:
1 2 3 4 5 6 | [Serializable] public sealed class Person { public string Name; public DateTime DateOfBirth; [NonSerialized] public int Age; } |
Несериализованные члены при десериализации всегда получают пустое значение или null, даже если инициализаторы полей и конструкторы устанавливают их по другому.
Атрибут [OptionalField]
По умолчанию добавление нового поля нарушает совместимость с уже сериализованными данными и сериализатор выбросит исключение. Этого можно избежать добавив к новому полю атрибут [OptionalField]
:
1 2 3 4 5 | [Serializable] public sealed class Person { public string Name; [OptionalField (VersionAdded = 2)] public DateTime DateOfBirth; } |
Это указывает сериализатору не генерировать исключение, если в потоке сериализованных данных он не встреит помеченного поля, а считать поле просто не сериализованным и оставить его пустым (ему можно затем присвоить значение с помощью метода [OnDeserializing]
).
Аргумент VersionAdded
— это целочисленное значение, инкрементируемое при каждом добавлении к типу новых полей. Оно носит скорее справочный характер и никак не влияет на сериализацию.
Вообще в целях сохранения версий рекомендуется избегать переименования, удаления и изменения типа полей.
Когда BinaryFormatter
при десериализации обнаруживает в потоке сериализованных данных поле, которое не объявлено в типе, он его просто отбросит, аSoapFormatter
сгененриует исключение.
Интерфейс ISerializable
Реализация интерфейса ISerializable
предоставляет типу полный контроль над тем, как производится его двоичная сериализая и десериализация. Определение интерфейса выглядит следующим образом:
1 2 3 4 | public interface ISerializable { void GetObjectData (SerializationInfo info, StreamingContext context); } |
Метод GetObjectData
запускается при сериализации и наполняет объект SerializationInfo
(словарь пар имя-значение) данными из всех полей, подлежащих сериализации. Пример реализации для типа с двумя сериализуемыми полями Name
и DateOfBirth
:
1 2 3 4 5 6 | public virtual void GetObjectData (SerializationInfo info, StreamingContext context) { info.AddValue ("Name", Name); info.AddValue ("DateOfBirth", DateOfBirth); } |
В примере ключи значений объекта SerializationInfo
совпадают с именами полей в типе, но это не является обязательным: допустимо использовать любые строковые ключи, при условии, что конструктор десериализации сможет их преобразовать в имена полей. Сами значения могут относится к любому сериализуемому типу: при необходимости будет выполнена рекурсивная сериализация. Также допустимо если значением в словаре будет null
.
Рекомендуется объявлять метод GetObjectData
как virtual
, чтобы производные классы могли расширять сериализацию не реализуя заново интрефейсISerializable
.
Класс SerializationInfo
также содержит свойства позволяющие управлять типом и сборкой, в которые должен быть помещен десериализованный экземпляр.
Параметр StreamingContext
— это структрура, по мимо прочего содержащая значения enum, указывающие откуда поступает сериализованный экземпляр.
Помимо реализации интерфейса ISerializable
, тип, управляющий собственной сериализацией должен содержать конструктор десериализации, принимающий те же два параметра, что и метод GetObjectData
. Он может быть объявлен с любым модификатором доступа.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [Serializable] public class Team : ISerializable { public string Name; public List<Person> Players; public virtual void GetObjectData (SerializationInfo si, StreamingContext sc) { si.AddValue ("Name", Name); si.AddValue ("PlayerData", Players.ToArray()); } public Team() {} // Конструктор десериализации protected Team (SerializationInfo si, StreamingContext sc) { Name = si.GetString ("Name"); Person[] a = (Person[]) si.GetValue ("PlayerData", typeof (Person[])); Players = new List<Person> (a); } } |
Для ряда типов класс SerializationInfo
имеет специальные методы Get*
, например, GetString
, для более удобного получения значений по ключам. Если объект SerializationInfo
не имеет значения с запрошенным ключом, будет сгенерировано исключение.
XML сериализатор
В пространстве имен System.Xml.Serialization
определен еще один сериализатор — XmlSerializer
. Он позволяет сериализовать типы в XML файлы. Как и при двоичной сериализации доступно два подхода:
- добавлять к типу атрибуты из пространства имен
System.Xml.Serialization
- реализовывать интерфейс
IXmlSerializable
В отличае от двоичной сериализации, реализация интерфейса IXmlSerializable
полностью исключает применение встроенного сериализатора, оставляя разработчику самостоятельное написание кода сериализации с использованием XmlReader
и XmlWriter
.
Выполнение XML сериализации
Для использования XmlSerializer
необходимо создать его экземпляр и вызвать на нем Serialize
или Deserialize
, передав им в качестве аргументов поток (Stream
), в который будут записываться сериализуемые данные (или из которого они будут читаться) и сериализуемый (или десериализуемый) объект:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Сериализуемый тип: public class Person { public string Name; public int Age; } // Сериализуемый объект: Person p = new Person(); p.Name = "Stacey"; p.Age = 30; // Сериализация: XmlSerializer xs = new XmlSerializer (typeof (Person)); using (Stream s = File.Create ("person.xml")) xs.Serialize (s, p); // Десериализация: Person p2; using (Stream s = File.OpenRead ("person.xml")) p2 = (Person) xs.Deserialize (s); Console.WriteLine (p2.Name + " " + p2.Age); // Stacey 30 |
Методы Serialize
и Deserialize
могут работать с Stream
,XmlWriter
/XmlReader
или TextWriter
/TextReader
.
Для сериализации с помощью XmlSerializer
добавлять сериализуемому типу какие-либо атрибуты не требуется. По умолчанию сериализуются все открытые поля и свойства типа. Исключить члены из сериализации можно с помощью атрибута [XmlIgnore]
:
1 2 3 4 5 | public class Person { ... [XmlIgnore] public DateTime DateOfBirth; } |
XmlSerializer
не распознает атрибут [OnDeserializing]
, а вместо него при десериализации использует конструктор без парарметров (в том числе неявно заданный) и генерирует исключение если он не найден. Также при десериализации выполняются инициализаторы полей:
1 2 3 4 | public class Person { public bool Valid = true; // Будет выполнено при десриализации } |
Следующие типы обрабатываются специальным образом:
- примитивные типы,
DateTime
,TimeSpan
,Guid
и их версии, допускающие значение null вставляются как значения byte[]
— преобразуется в base 64XmlAttribute
иXmlElement
— их контент непосредственно вставляется в XML (без обработки)- любой тип, реализующий
IXmlSerializable
— обрабатывается в соответствии с собственной реализацией - все типы коллекций
XmlSerializer
обладает хорошей совместимостью: при десериализации он не жалуется если элементы или атрибуты отсутствуют либо встречаются лишние данные.
По умолчанию члены сериализуются в XML элементы. Если к члену добавить атрибут [XmlAttribute]
, он будет сериализован в XML атрибут:
1 | [XmlAttribute] public int Age; |
С помощью атрибутов можно изменять стандартные имена элементов и атрибутов:
1 2 3 4 5 | public class Person { [XmlElement ("FirstName")] public string Name; [XmlAttribute ("RoughAge")] public int Age; } |
Будет сериализовано:
1 2 3 | <Person RoughAge="30" ...> <FirstName>Stacey</FirstName> </Person> |
Стандартное пространство имен является пустым, задать его можно с помощью аргумента Namespace
атрибутов [XmlElement]
и [XmlAttribute]
. Можно также назначить имя и пространство имен самому типу с помощью атрибута [XmlRoot]
:
1 2 | [XmlRoot ("Candidate", Namespace = "http://mynamespace/test/")] public class Person { ... } |
XmlSerializer
записывает элементы в порядке, в котором они определены в классе. Изменить этот порядок можно с помощью аргумента Order
элемента XmlElement
:
1 2 3 4 5 | public class Person { [XmlElement (Order = 2)] public string Name; [XmlElement (Order = 1)] public int Age; } |
Если аргумент Order
используется, он должен быть проставлен для всех элементов.
Обработка производных классов и объектных ссылок
Для того чтобы XmlSerializer
мог корректно сериализовать и десериализовать производные классы, ему необходимо сообщить об их существовании. Сделать это можно двумя способами:
- с помощью атрибута
[XmlInclude]
, применяемого к базовому сериализуемому типу:123[XmlInclude (typeof (Student))][XmlInclude (typeof (Teacher))]public class Person { public string Name; } - указать производные типы при создании экземпляра
XmlSerializer
:12XmlSerializer xs = new XmlSerializer (typeof (Person),new Type[] { typeof (Student), typeof (Teacher) } );
В обоих случаях сериализатор запишет производный тип в атрибут type
:
1 2 3 4 | <Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Student"> <Name>Stacey</Name> </Person> |
При десериализации на основании значения этого атрибута десериализатор создаст объект нужного производного типа.
Именем, записываемым в XML атрибут type
, можно управлять, применяя к производному типу атрибут [XmlType]
:
1 2 | [XmlType ("Candidate")] public class Student : Person { } |
XmlSerializer
автоматически рекурсивно обрабатывает объектные ссылки:
1 2 3 4 5 6 7 8 9 | public class Person { public string Name; public Address HomeAddress = new Address(); } public class Address { public string Street, PostCode; } Person p = new Person(); p.Name = "Stacey"; p.HomeAddress.Street = "Odo St"; p.HomeAddress.PostCode = "6020"; |
Будет сериализовано:
1 2 3 4 5 6 7 | <Person ... > <Name>Stacey</Name> <HomeAddress> <Street>Odo St</Street> <PostCode>6020</PostCode> </HomeAddress> </Person> |
Если на один и тот же объект ссылаются два поля, объект будет сериализован дважды.
Если поле ссылается на объект производного типа, можно управлять тем, какое имя получит соответствующий XML элемент. Например, для следующей ситуации:
1 2 3 4 5 6 7 8 | public class Address { public string Street, PostCode; } public class USAddress : Address { } public class AUAddress : Address { } public class Person { public string Name; public Address HomeAddress = new USAddress(); } |
если зарегистрировать каждый производный класс, присвоив атрибуты [XmlInclude]
базовому классу:
1 2 3 4 5 6 | [XmlInclude (typeof (AUAddress))] [XmlInclude (typeof (USAddress))] public class Address { public string Street, PostCode; } |
то имя XML элемента будет соответствовать имени ссылающегося поля или свойства, а реальный производный тип объекта будет записан в атрибут type
:
1 2 3 4 5 6 | <Person ...> ... <HomeAddress xsi:type="USAddress"> ... </HomeAddress> </Person> |
Если к ссылающемуся полю или свойству применить несколько атрибутов [XmlElement]
:
1 2 3 4 5 6 7 8 | public class Person { public string Name; [XmlElement ("Address", typeof (Address))] [XmlElement ("AUAddress", typeof (AUAddress))] [XmlElement ("USAddress", typeof (USAddress))] public Address HomeAddress = new USAddress(); } |
то каждый атрибут [XmlElement]
сопоставит имя элемента с типом и имя XML элемента будет совпадать с именем производного типа:
1 2 3 4 5 6 | <Person ...> ... <USAddress> ... </USAddress> </Person> |
При этом если в атрибуте [XmlElement]
не указывать имя, а только тип, будет использовано стандартное имя типа (которое можно изменить с помощью [XmlType]
).
Сериализация коллекций
XmlSerializer
способен сериализовать коллекции без каких либо дополнительных настроек:
1 2 3 4 5 6 | public class Person { public string Name; public List<Address> Addresses = new List<Address>(); } public class Address { public string Street, PostCode; } |
Будет сериализовано в:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <Person ... > <Name>...</Name> <Addresses> <Address> <Street>...</Street> <Postcode>...</Postcode> </Address> <Address> <Street>...</Street> <Postcode>...</Postcode> </Address> ... </Addresses> </Person> |
Атрибут [XmlArray]
позволяет переименовать внешний элемент, а [XmlArrayItem]
— внутренние элементы:
1 2 3 4 5 6 7 | public class Person { public string Name; [XmlArray ("PreviousAddresses")] [XmlArrayItem ("Location")] public List<Address> Addresses = new List<Address>(); } |
Будет сериализовано в:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <Person ... > <Name>...</Name> <PreviousAddresses> <Location> <Street>...</Street> <Postcode>...</Postcode> </Location> <Location> <Street>...</Street> <Postcode>...</Postcode> </Location> ... </PreviousAddresses> </Person> |
Оба атрибута также позволяют указывать пространство имен.
Чтобы сериализовать коллекцию без внешнего элемента, необходимо к ссылающемуся на коллекцию полю добавить атрибут [XmlElement]
с аргументом имени, совпадающим с типом элементов коллекции:
1 2 3 4 5 6 | public class Person { ... [XmlElement ("Address")] public List<Address> Addresses = new List<Address>(); } |
Если коллекция содержит элементы производных классов, то правил именования XML элементов следующие:
- чтобы включить имя производного типа в атрибут
type
нужно добавить атрибут[XmlInclude]
к базовому типу элементов коллекции (как это делалось выше):1234<Person ... ><Name>...</Name><Addresses><Address xsi:type="AUAddress"> - если элементы коллекции должны именоваться в соответствии с их реальным типом (производным), необходимо применить несколько атрибутов
[XmlArrayItem]
или[XmlElement]
к ссылающемуся на коллекцию полю:При этом если использовать атрибут12345678910111213<Person ... ><Name>...</Name><!-начало необязательного внешнего элемента-><AUAddress><Street>...</Street><Postcode>...</Postcode></AUAddress><USAddress><Street>...</Street><Postcode>...</Postcode></USAddress><!-конец необязательного внешнего элемента-></Person>[XmlArrayItem]
, внешний элемент будет включен в XML:А если использовать атрибут1234[XmlArrayItem ("Address", typeof (Address))][XmlArrayItem ("AUAddress", typeof (AUAddress))][XmlArrayItem ("USAddress", typeof (USAddress))]public List<Address> Addresses = new List<Address>();[XmlElement]
, внешний элемент будет исключен из коллекции:1234[XmlElement ("Address", typeof (Address))][XmlElement ("AUAddress", typeof (AUAddress))][XmlElement ("USAddress", typeof (USAddress))]public List<Address> Addresses = new List<Address>();
IXmlSerializable
Реализация интерфейса IXmlSerializable
дает значительно больше возможностей по управлению сериализацией и десериализацией, чем использование атрибутов. Объявление интерфейса выглядит следующим образом:
1 2 3 4 5 6 | public interface IXmlSerializable { XmlSchema GetSchema(); void ReadXml (XmlReader reader); void WriteXml (XmlWriter writer); } |
Метод ReadXml
должен читать внешний начальный элемент, затем содержимое и внешний конечный элемент. Метод WriteXml
должен записывать только содержимое.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; public class Address : IXmlSerializable { public string Street, PostCode; public XmlSchema GetSchema() { return null; } public void ReadXml(XmlReader reader) { reader.ReadStartElement(); Street = reader.ReadElementContentAsString ("Street", ""); PostCode = reader.ReadElementContentAsString ("PostCode", ""); reader.ReadEndElement(); } public void WriteXml (XmlWriter writer) { writer.WriteElementString ("Street", Street); writer.WriteElementString ("PostCode", PostCode); } } |
XmlSerializer
при сериализации и десериализации будет автоматически вызывать методы WriteXml
и ReadXml
.