Сериализация (Serialization) в C#

Сериализация — процесс преобразования объекта или группы связанных объектов в поток байт или набор 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 код. Требует предварительной явной регистрации сериализуемых производных типов, чтобы иметь возможность сопоставлять имена контрактов данных с именами типов .NET
  • NetDataContractSerializer — характеризуется тесной привязкой типов .NET к типам контрактов данных, не требует явной регистрации сериализуемых производных типов, т.к. самостоятельно записывает полные имена типов и сборок сериализуемых типов

После выбора сериализатора необходимо всем сериализуемым типам добавить атрибут[DataContract], а их членам, которые необходимо включить в сериализацию, — атрибуты [DataMember]:

После этого можно явно сериализовать и десериализовать объекты, создавая экземпляр DataContractSerializer или NetDataContractSerializer и вызывая метод WriteObject или ReadObject:

Конструктору DataContractSerializer необходимо передать тип корневого объекта — сериализуемый объект, который в XML дереве будет корневым элементом. Конструктор NetDataContractSerializer этого не требует. В остальном их применение аналогично.

Оба типа сериализаторов по умолчанию принимаю форматер XML, поэтому с ними можно использовать XmlWriter:

Имена XML элементов в выводе соответствуют именам контрактов данных, которые по умолчанию совпадают с именами типов в .NET, однако это можно переопределить с помощью атрибутов, задав альтернативное имя для элемента:

Также можно изменить пространство имен по умолчанию для корневого элемента:

Можно также переопределить имена членов данных:

Атрибут [DataMember] может быть применен к частным и публичным полям и свойствам следующих типов данных:

  • любой примитивный тип
  • DateTime, TimeSpan, Guid, Uri или Enum
  • типы, допускающие значение null вышеуказанных типов
  • byte[] (сериализуется в XML с применением кодировки Base64
  • любой тип с атрибутом [DataContract]
  • любой тип IEnumerable
  • любой тип с атрибутом [Serializable] или реализующий интерфейс ISerializable
  • любой тип реализующий интерфейс IXmlSerializable

Можно использовать двоичный форматер:

Сериализация производных типов

При сериализации производных типов если используется NetDataContractSerializer никаких дополнительных действий не требуется, необходимо только снабдить производный класс атрибутом[DataContract]. Сериализатор будет записывать полностью определенные имена типов:

 

В случае с DataContractSerializer сериализатору необходимо сообщить обо всех производных типах, которые он может сериализовать. Если этого не сделать при десерриализации возникнет ошибка, т.к. сериализатор не сможет узнать в какой именно тип следует преобразовать элемент:

Сообщить сериализатору о производных типах можно при создании экземпляра DataContractSerializer, либо в самом типе с помощью атрибута[KnownType]:

Сериализованный объект Student будет выглядеть так:

Поскольку в качестве корневого типа указан Person, корневой элемент имеет его имя, а действительный подкласс описан отдельно в атрибуте type.

Объектные ссылки

Ссылки на другие объекты также сериализуются. Напрмер:

Будет сериализовано в:

Класс NetDataContractSerializer всегда сохраняет эквивалентность ссылок на объекты. Класс DataContractSerializer по умолчанию этого не делает. Это означает, что если на один и тот же объект имеются ссылки в двух разных местах, DataContractSerializer запишет его дважды. Например, для такого объекта:

XML будет содержать тот же самый адрес дважды:

При последующей десериализации WorkAddress и HomeAddress будут преобразованы в два разных объекта. Преимуществом такого подходя является простота и совместимость XML, а недостатком — большой размер XML, потеря ссылочной целостности и невозможность справиться с циклическими ссылками.

Чтобы сохранить ссылочную эквивалентность, нужно в конструктор DataContractSerializer для аргумента preserveObjectReferen передать true:

При этом третьим аргументом необходимо передать максимальное количество объектных ссылок, которые сериализатор должен отслеживать (при привышении этого количество будет сгенерировано исключение). XML при сохранении ссылочной эквивалентности будет выглядеть так:

Совместимость версий

Можно добавлять и удалять члены в сериализуемые типы, не нарушая при этом прямой и обратной совместимости. Сериализаторы всегда пропускают при сериализации члены, для которых не установлен атрибут [DataMember], а при десериализации не генерируют исключений, если для члена, снабженного атрибутом [DataMember], нет сериализованных данных. Нераспознанные данные, присутствующие в потоке сериализации, но отсутствующие у десериализуемого типа, также молча пропускаются. Однако если тип реализует интерфейс IExtensibleDataObject, нераспознанные данные пропускаться не будут, а будут сохраняться в объект ExtensionDataObject, который может быть получен с помощью свойства ExtensionData:

Если член является важным для объекта можно потребовать его присутствия с помощью IsRequired:

Если такой член отсутствует, при десериализации будет сгенерировано исключение.

Упорядочение членов

Порядок следования членов при сериализации и десериализации играет определенное значение: при десериализации неправильно упорядоченные члены будут пропущены.

Упорядочение осуществляется при сериализации по следующим правилам:

  • сначала сериализуются члены базового класса, потом производного
  • для членов с установленным аргументом Order учитывается его значение (от меньшего к большему)
  • в последнюю очередь учитывается алфавитный порядок имен членов

Таким образом поле Age будет предшествовать полю Name, если только не поменять их порядок с помощью аргумента Order:

Пустые значения и null

Члены типа, значениями которых является null или пустые значения, при сериализации по умолчанию все равно записываются с пустым значением:

Однако это не всегда целесообразно, поскольку ведет к бесполезному расходу пространства особенно для типов с множеством пустых полей и свойств. Поэтому сериализатору можно сообщить о необходимости пропускать пустые поля и свойства. Сделать это можно с помощью аргумента EmitDefaultValue:

В результате Name будет пропущен, если его значение равно null, а Age будет пропущен, если его значение равно 0.

Сериализация коллекций

Сериализаторы контрактов данных могут сериализовать и десериализовать любую коллекцию:

Вывод:

При этом сериализатор не кодирует информацию о конкретном типе коллекции. Это позволяет при десериализации изменить тип коллекции без генерации ошибок.

При сериализации кастомного типа коллекции можно настроить имя XML узла для каждого элемента коллекции. Для этого используется атрибут[CollectionDataContract]:

Вывод будет таким:

Атрибут [CollectionDataContract] также позволяет задать аргументы Namespace и Name. Аргумент Name не используется, когда коллекция сериализуется как свойство другого объекта, но применяется если коллекция сериализуется как корневой объект.

Атрибут [CollectionDataContract] может также использоваться для управления сериализацией словарей:

Вывод:

Хуки сериализации

Непосредственно до и после сериализации или десерализации можно выполнить специальный метод. Задать такой метод можно с помощью следующих атрибутов:

  • [OnSerializing] — указывает метод для вызова перед сериализацией
  • [OnSerialized] — указывает метод для вызова после сериализации
  • [OnDeserializing] — указывает метод для вызова перед десериализацией
  • [OnDeserialized] — указывает метод для вызова после десериализации

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

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

Методы могут быть как публичными так и частными. Производные типы могут определять свои хуки и они также будт вызваны.

Поддержка механизмов двоичной сериализации

Сериализаторы контрактов данных также может сериализовать типы помеченные атрибутом [Serializable] и реализующие интерфейс ISerializable. При этом не происходит переключения на двоичную сериализацию, но многие механизмы работают ка при двоичной сериализации: учитываются атрибуты [NonSerialized], выполняются методы GetObjectData и конструкторы десериализации.

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

Двоичный сериализатор

Сделать тип поддерживающим двоичную сериализацию можно двумя путями: 

  • добавить типу атрибут [Serializable]
  • реализовать в типе интерфейс ISerializable

Добавление атрибута проще, но реализация интерфейса дает больше возможностей.

Атрибут [Serializable]

Сделать тип сериализуемым можно с помощью единственного атрибута:

Атрибут [Serializable] указывает сериализатору на необходимость включать все поля данного типа, как публичные так и частные, но не включать свойства.

Каждое поле должно иметь сериализуемый тип, т.е. тип также помеченный атрибутом [Serializable], либо реализующий интерфейс ISerializable. Примитивные типы и множество других типов .NET являются сериализуемыми.

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

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

Выполнение двоичной сериализации

Чтобы выполнить двоичную сериализацию необходимо создать объект форматера и вызвать его метод Serialize. Для двоичной сериализации предусмотрено два форматера:

  • BinaryFormatter — более эффективный, генерирует небольшой вывод за меньшее время. Определен в пространстве имен System.Runtime.Serialization.Formatters.Binary
  • SoapFormatter — поддерживает базовый обмен сообщениями, менее функционален, не поддерживает сериализацию обобщенных типов и фильтрацию посторонних данных. Определен в пространстве имен System.Runtime.Serialization.Formatters.Soap

Оба форматера используются одинаково:

Метод Deserialize восстанавливает объект:

При воссоздании объектов десериализатор пропускает все конструкторы.

Сериализованные данные включают полные сведения о типе и сборке, поэтому если при десереиализации попытаться привести тип к совместимому типу, возникнет ошибка. Десериализатор полностью восстанавливает объектные ссылки в их исходное состояние.

Двоичные сериализаторы поддерживают также атрибуты [OnSerializing],[OnSerialized], [OnDeserializing] и [OnDeserialized]. Их применение не отличается от сериализаторов контрактов данных.

Атрибут [NonSerialized]

В отличие от сериализаторов контрактов данных, которые требую помечать атрибутами все сериализуемые поля, двоичный сериализатор этого не требует и по умолчанию включает все поля сериализуемого объекта. Исключить отдельные поля из сериализации можно отметив их атрибутом [NonSerialized]:

Несериализованные члены при десериализации всегда получают пустое значение или null, даже если инициализаторы полей и конструкторы устанавливают их по другому.

Атрибут [OptionalField]

По умолчанию добавление нового поля нарушает совместимость с уже сериализованными данными и сериализатор выбросит исключение. Этого можно избежать добавив к новому полю атрибут [OptionalField]:

Это указывает сериализатору не генерировать исключение, если в потоке сериализованных данных он не встреит помеченного поля, а считать поле просто не сериализованным и оставить его пустым (ему можно затем присвоить значение с помощью метода [OnDeserializing]).

Аргумент VersionAdded — это целочисленное значение, инкрементируемое при каждом добавлении к типу новых полей. Оно носит скорее справочный характер и никак не влияет на сериализацию.

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

Когда BinaryFormatter при десериализации обнаруживает в потоке сериализованных данных поле, которое не объявлено в типе, он его просто отбросит, аSoapFormatter сгененриует исключение.

Интерфейс ISerializable

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

Метод GetObjectData запускается при сериализации и наполняет объект SerializationInfo (словарь пар имя-значение) данными из всех полей, подлежащих сериализации. Пример реализации для типа с двумя сериализуемыми полями Name и DateOfBirth:

В примере ключи значений объекта SerializationInfo совпадают с именами полей в типе, но это не является обязательным: допустимо использовать любые строковые ключи, при условии, что конструктор десериализации сможет их преобразовать в имена полей. Сами значения могут относится к любому сериализуемому типу: при необходимости будет выполнена рекурсивная сериализация. Также допустимо если значением в словаре будет null.

Рекомендуется объявлять метод GetObjectData как virtual, чтобы производные классы могли расширять сериализацию не реализуя заново интрефейсISerializable.

Класс SerializationInfo также содержит свойства позволяющие управлять типом и сборкой, в которые должен быть помещен десериализованный экземпляр.

Параметр StreamingContext — это структрура, по мимо прочего содержащая значения enum, указывающие откуда поступает сериализованный экземпляр.

Помимо реализации интерфейса ISerializable, тип, управляющий собственной сериализацией должен содержать конструктор десериализации, принимающий те же два параметра, что и метод GetObjectData. Он может быть объявлен с любым модификатором доступа.

Для ряда типов класс SerializationInfo имеет специальные методы Get*, например, GetString, для более удобного получения значений по ключам. Если объект SerializationInfo не имеет значения с запрошенным ключом, будет сгенерировано исключение.

XML сериализатор

В пространстве имен System.Xml.Serialization определен еще один сериализатор — XmlSerializer. Он позволяет сериализовать типы в XML файлы. Как и при двоичной сериализации доступно два подхода:

  • добавлять к типу атрибуты из пространства имен System.Xml.Serialization
  • реализовывать интерфейс IXmlSerializable

В отличае от двоичной сериализации, реализация интерфейса IXmlSerializableполностью исключает применение встроенного сериализатора, оставляя разработчику самостоятельное написание кода сериализации с использованием XmlReaderи XmlWriter.

Выполнение XML сериализации

Для использования XmlSerializer необходимо создать его экземпляр и вызвать на нем Serialize или Deserialize, передав им в качестве аргументов поток (Stream), в который будут записываться сериализуемые данные (или из которого они будут читаться) и сериализуемый (или десериализуемый) объект:

Методы Serialize и Deserialize могут работать с Stream,XmlWriter/XmlReader или TextWriter/TextReader.

Для сериализации с помощью XmlSerializer добавлять сериализуемому типу какие-либо атрибуты не требуется. По умолчанию сериализуются все открытые поля и свойства типа. Исключить члены из сериализации можно с помощью атрибута [XmlIgnore]:

XmlSerializer не распознает атрибут [OnDeserializing], а вместо него при десериализации использует конструктор без парарметров (в том числе неявно заданный) и генерирует исключение если он не найден. Также при десериализации выполняются инициализаторы полей:

Следующие типы обрабатываются специальным образом:

  • примитивные типы, DateTime, TimeSpan, Guid и их версии, допускающие значение null вставляются как значения
  • byte[] — преобразуется в base 64
  • XmlAttribute и XmlElement — их контент непосредственно вставляется в XML (без обработки)
  • любой тип, реализующий IXmlSerializable — обрабатывается в соответствии с собственной реализацией
  • все типы коллекций

XmlSerializer обладает хорошей совместимостью: при десериализации он не жалуется если элементы или атрибуты отсутствуют либо встречаются лишние данные.

По умолчанию члены сериализуются в XML элементы. Если к члену добавить атрибут [XmlAttribute], он будет сериализован в XML атрибут:

С помощью атрибутов можно изменять стандартные имена элементов и атрибутов:

Будет сериализовано:

Стандартное пространство имен является пустым, задать его можно с помощью аргумента Namespace атрибутов [XmlElement] и [XmlAttribute]. Можно также назначить имя и пространство имен самому типу с помощью атрибута [XmlRoot]:

XmlSerializer записывает элементы в порядке, в котором они определены в классе. Изменить этот порядок можно с помощью аргумента Order элемента XmlElement:

Если аргумент Order используется, он должен быть проставлен для всех элементов.

Обработка производных классов и объектных ссылок

Для того чтобы XmlSerializer мог корректно сериализовать и десериализовать производные классы, ему необходимо сообщить об их существовании. Сделать это можно двумя способами:

  • с помощью атрибута [XmlInclude], применяемого к базовому сериализуемому типу:
  • указать производные типы при создании экземпляра XmlSerializer:

 В обоих случаях сериализатор запишет производный тип в атрибут type:

При десериализации на основании значения этого атрибута десериализатор создаст объект нужного производного типа.

Именем, записываемым в XML атрибут type, можно управлять, применяя к производному типу атрибут [XmlType]:

XmlSerializer автоматически рекурсивно обрабатывает объектные ссылки:

Будет сериализовано:

Если на один и тот же объект ссылаются два поля, объект будет сериализован дважды.

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

если зарегистрировать каждый производный класс, присвоив атрибуты [XmlInclude] базовому классу:

то имя XML элемента будет соответствовать имени ссылающегося поля или свойства, а реальный производный тип объекта будет записан в атрибут type:

Если к ссылающемуся полю или свойству применить несколько атрибутов [XmlElement]:

то каждый атрибут [XmlElement] сопоставит имя элемента с типом и имя XML элемента будет совпадать с именем производного типа:

При этом если в атрибуте [XmlElement] не указывать имя, а только тип, будет использовано стандартное имя типа (которое можно изменить с помощью [XmlType]).

Сериализация коллекций

XmlSerializer способен сериализовать коллекции без каких либо дополнительных настроек:

Будет сериализовано в:

Атрибут [XmlArray] позволяет переименовать внешний элемент, а [XmlArrayItem] — внутренние элементы:

Будет сериализовано в:

Оба атрибута также позволяют указывать пространство имен.

Чтобы сериализовать коллекцию без внешнего элемента, необходимо к ссылающемуся на коллекцию полю добавить атрибут [XmlElement] с аргументом имени, совпадающим с типом элементов коллекции:

 Если коллекция содержит элементы производных классов, то правил именования XML элементов следующие:

  • чтобы включить имя производного типа в атрибут type нужно добавить атрибут [XmlInclude] к базовому типу элементов коллекции (как это делалось выше):
  • если элементы коллекции должны именоваться в соответствии с их реальным типом (производным), необходимо применить несколько атрибутов [XmlArrayItem]или [XmlElement] к ссылающемуся на коллекцию полю:

    При этом если использовать атрибут [XmlArrayItem], внешний элемент будет включен в XML:

    А если использовать атрибут [XmlElement], внешний элемент будет исключен из коллекции:

IXmlSerializable

Реализация интерфейса IXmlSerializable дает значительно больше возможностей по управлению сериализацией и десериализацией, чем использование атрибутов. Объявление интерфейса выглядит следующим образом:

Метод ReadXml должен читать внешний начальный элемент, затем содержимое и внешний конечный элемент. Метод WriteXml должен записывать только содержимое.

XmlSerializer при сериализации и десериализации будет автоматически вызывать методы WriteXml и ReadXml.

Один комментарий к “Сериализация (Serialization) в C#”

  1. Огромное спасибо за статью. По этой теме немного есть хороших материалов.
    Может быть подскажете ещё один момент: как записать текст в тело элемента? т.е. есть класс:
    [System.Xml.Serialization.XmlType(TypeName = «Cell»)]
    public class XCell
    {
    [System.Xml.Serialization.XmlAttribute()]
    public string Id;
    public string Value;
    }
    при таком описании объект типа XCell сериализуется в :

    123,45

    как сделать так, чтобы получилось
    123,45
    спасибо

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *