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

95

В большинстве случаев схема сериализации по умолчанию, предоставляемая платформой .NET, вполне подходит. Нужно лишь применить атрибут [Serializable] к связанным типам и передать дерево объектов выбранному форматеру для обработки.

Однако в некоторых случаях может понадобиться вмешаться в процесс конструирования дерева и процесс сериализации. Например, может существовать бизнес-правило, которое гласит, что все поля данных должны сохраняться в определенном формате, или же необходимо добавить дополнительные данные в поток, которые напрямую не отображаются на поля сохраняемого объекта (временные метки, уникальные идентификаторы и т.п.).

В пространстве имен System.Runtime.Serialization предусмотрено несколько типов, которые позволяют вмешаться в процесс сериализации объектов. В таблице описаны некоторые из основных типов:

ISerializable Этот интерфейс может быть реализован на типе [Serializable] для управления его сериализацией и десериализацией
ObjectIDGenerator Этот тип генерирует идентификаторы для членов графа объектов
[OnDeserialized] Этот атрибут позволяет указать метод, который будет вызван немедленно после десериализации объекта
[OnDeserializing] Этот атрибут позволяет указать метод, который будет вызван перед процессом десериализации
[OnSerialized] Этот атрибут позволяет указать метод, который будет вызван немедленно после того, как объект сериализован
[OnSerializing] Этот атрибут позволяет указать метод, который будет вызван перед процессом сериализации
[OptionalField] Этот атрибут позволяет определить поле типа, которое может быть пропущено в указанном потоке
SerializationInfo По существу это "мешок свойств", который поддерживает пары "имя/значение", представляющие состояние объекта во время процесса сериализации

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

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

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

В дополнение к определению того, поддерживает ли тип ISerializable, форматеры также отвечают за исследование типов на предмет поддержки членов, которые оснащены атрибутами [OnSerializing], [OnSerialized], [OnDeserializing] или [OnDeserialized]. Мы рассмотрим назначение этих атрибутов чуть позже, а сначала давайте посмотрим на предназначение ISerializable.

Настройка сериализации с использованием интерфейса ISerializable

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

После выхода версии .NET 2.0 предпочтительный способ настройки процесса сериализации начал предусматривать использование атрибутов сериализации. Тем не менее, знание интерфейса ISerializable важно для сопровождения систем, которые уже существуют. Интерфейс ISerializable довольно прост, учитывая, что в нем определен единственный метод GetObjectData().

Метод GetObjectData() вызывается автоматически заданным форматером во время процесса сериализации. Реализация этого метода заполняет входной параметр SerializationInfo последовательностью пар "имя/значение", которые (обычно) отображают данные полей сохраняемого объекта. В SerializationInfo определены многочисленные вариации перегруженного метода AddValue(), а также небольшой набор свойств, которые позволяют устанавливать и получать имя типа, определять сборку и счетчик членов.

Типы, реализующие интерфейс ISerializable, также должны определять специальный конструктор со следующей сигнатурой:

// Необходимо предусмотреть специальный конструктор с такой сигнатурой,
// чтобы позволить исполняющей среде устанавливать состояние объекта.
[Serializable]
class SomeClass : ISerializable
{
   protected SomeClass (SerializationInfo si, StreamingContext ctx) {...}
}

Обратите внимание, что видимость конструктора указана как protected. Это приемлемо, учитывая, что форматер будет иметь доступ к этому члену независимо от его видимости. Такие специальные конструкторы обычно делают protected (или private), тем самым гарантируя, что небрежный пользователь объекта никогда не создаст объект подобным образом. Как видите, первым параметром конструктора является экземпляр типа SerializationInfo.

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

Значения этого перечисления представляют базовую композицию текущего потока. Если только не планируется реализовать какие-то низкоуровневые службы удаленного взаимодействия, то иметь дело с этим перечислением непосредственно требуется редко (подробности ищите в документации .NET Framework 4.0 SDK).

Настройка сериализации с использованием атрибутов

Хотя реализация интерфейса ISerializable является одним из возможных способов настройки процесса сериализации, с момента выхода версии .NET 2.0 предпочтительным способом такой настройки стало определение методов, оснащенных атрибутами из следующего перечня: [OnSerializing], [OnSerialized], [OnDeserializing] и [OnDeserialized]. Использование этих атрибутов дает менее громоздкий код, чем реализация интерфейса ISerializable, учитывая, что не приходится вручную взаимодействовать с параметром SerializationInfo. Вместо этого можно напрямую модифицировать данные состояния, когда форматер работает с вашим типом.

Эти атрибуты сериализации определены в пространстве имен System.Runtime.Serialization.

В случае применения этих атрибутов метод должен быть определен так, чтобы принимать параметр StreamingContext и не возвращать ничего (иначе будет сгенерировано исключение времени выполнения). Обратите внимание, что применять каждый из атрибутов сериализации не обязательно, а можно просто вмешаться в те стадии процесса сериализации, которые интересуют. Для иллюстрации ниже приведен новый тип [Serializable]:

[Serializable]
class MoreData
{
   private string dataItemOne = "First data block";
   private string dataItemTwo= "More data";
   [OnSerializing]
   private void OnSerializing(StreamingContext context)
   {
      // Вызывается во время процесса сериализации.
      dataItemOne = dataItemOne.ToUpper();
      dataItemTwo = dataItemTwo.ToUpper();
   }
   
   [OnDeserialized]
   private void OnDeserialized(StreamingContext context)
   {
   // Вызывается по завершении процесса десериализации.
   dataltemOne = dataItemOne.ToLower();
   dataItemTwo = dataItemTwo.ToLower();
   }
}

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

Приведенный пример наглядно продемонстрировал основные детали служб сериализации объектов, включая различные способы настройки этого процесса. Как было показано, процессы сериализации и десериализации существенно упрощают задачу сохранения больших объемов данных, причем с меньшими затратами, чем работа с различными классами чтения/записи данных из пространства имен System.IO.

Пройди тесты
Лучший чат для C# программистов