вторник, 28 февраля 2012 г.

Сериализация в .NET - это просто

Магические слова “сериализация” и “десериализация” имеют отношение к магии сохранения состояния объекта. Наверняка сейчас мало у кого может возникнуть вопрос “зачем нужна сериализация”, но дабы соответствовать духу статьи, сказать об этом необходимо.
Сериализация имеет прямое отношение к сохранению состояния объекта в файле или памяти, а десериализация - к восстановлению состояния объекта соответственно. Примеров, когда это может пригодиться, неимоверное множество. Начиная от сохранения пользовательских настроек, сохранения промежуточного состояния объекта и заканчивая передачей объекта в специальном формате веб-сервису.
Для осуществления магии (де)сериализации .NET предлагает нам 3 родных варианта (не считая самостоятельной реализации механизма сериализации):
- Сериализация в двоичный формат (BinnaryFormatter)
- Сериализация в формат SOAP (SoapFormatter)
- Сериализация в формат xml (XmlSerializer)

У каждого из вариантов есть свои плюсы и минусы, о которых я постараюсь рассказать далее.

Создание класса для экспериментов
Чтобы сделать объект сериализируемым нужно снабдить каждый связанный с ним класс или структуру аттрибутом [Serializable]. Если есть поля, которые по какой-то причине нужно исключить из сериализации, их необходимо пометить аттрибутом [NonSerialized].
Прежде чем приступить к рассмотрению имеющихся механизмов сериализации, давайте подготовим для экспериментов парочку простеньких классов:
[Serializable]
public class Human
{
    private string name;
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    [NonSerialized] 
    public int age;
}
[Serializable]
public class SuperHuman : Human
{
    public bool isEvil { get; set; }
    public Ability superpower = new Ability();
    private bool hasSecret = true;

    public bool getHasSecret()
    {
        return hasSecret;
    }
}
[Serializable]
public class Ability
{
    public bool isUnique;
    public string[] abilities;
}
Сериализация в двоичный формат с помощью BinaryFormatter
BinaryFormatter сохраняет состояние объекта в двоичном формате. Сериализируются все поля, вне зависимости от их области видимости. Исключение составляют поля помеченные аттрибутом [NonSerialized]. Помимо сохранения данных полей, BinaryFormatter также сохраняет полное квалифицированное имя каждого типа, полное имя сборки, где он определен, сюда входит информация об имени, версии, маркере общедоступного ключа (public key) и культуре. Отсюда следует, что BinaryFormatter это идеальный выбор в ситуациях, когда необходимо сохранять полные копии объектов для дальнейшей их передачи по значению между доменами для использования в .NET приложениях. Здесь заключается основной минус BinaryFormatter - данные, сохраненные с его помощью, могут быть воссозданы только в инфраструктуре CLI. Причем каждый, кто будет восстанавливать данные, должен иметь сборку с сериализуемым типом.
Сериализация происходит с помощью двух ключевых методов Serialize() и Deserialize(). Первый сохраняет граф объектов в виде последовательности байт в указанный поток. Второй наоборот - преобразует сохраненную последовательность байт в граф объектов.
Теперь инициализируем описанный выше класс SuperHero и сохраним его состояние с помощью двоичной сериализации:
//Инициализация SuperHuman
SuperHuman superHuman = new SuperHuman();
superHuman.Name = "Invisible Sharpshooter";
superHuman.age = 20;
superHuman.isEvil = false;
superHuman.superpower.abilities = new string[] {"sharp eye", "invisibility"};
superHuman.superpower.isUnique = true;

//Сохраняем состояние объекта superHuman в двоичном формате
BinaryFormatter formatter = new BinaryFormatter();
using(var fStream = new FileStream("./SuperHumanInfo.dat", FileMode.Create, FileAccess.Write, FileShare.None))
{
    formatter.Serialize(fStream, superHuman);
}

//Восстановим состояние объекта
using(var fStream = File.OpenRead("./SuperHumanInfo.dat"))
{
    SuperHuman newSuperHuman = (SuperHuman)formatter.Deserialize(fStream);
    var skills = from abils in newSuperHuman.superpower.abilities select abils;

    Console.WriteLine("Name: {0}", newSuperHuman.Name);
    Console.WriteLine("age: {0}", newSuperHuman.age);
    Console.WriteLine("isEvil: {0}", newSuperHuman.isEvil);
    Console.WriteLine("isUnique: {0}", newSuperHuman.superpower.isUnique);
    Console.WriteLine("hasSecret: {0}", newSuperHuman.getHasSecret());
    Console.WriteLine("\nSkills List:");
    foreach (string skill in skills)
    {
        Console.WriteLine("\t" + skill);
    }
}
Вывод на консоль будет следующим:
Name: Invisible Sharpshooter
age: 0
isEvil: False
isUnique: True
hasSecret: True

Skills List:
 sharp eye
 invisibility
Из примера видно, что поле age не сохранилось и все благодаря аттрибуту [NonSerialized]. В то же самое время состояние закрытого поля hasSecret сохранено успешно.
Важно помнить, что метод Deserialize() возвращает объект типа System.Object, поэтому необходимо явно привести объект к нужному типу.

Сериализация в SOAP формат с помощью SoapFormatter
SoapFormatter сохраняет состояние объекта в SOAP формате (Simple Object Access Protocol) и также как и BinaryFormatter сериализирует все поля, вне зависимости от их области видимости, кроме полей помеченных аттрибутом [NonSerialized]. В отличие от BinaryFormatter, платформа и операционная система не влияют на успешное восстановление данных, сериализированных с помощью SoapFormatter.
Как и в случае с BinaryFormatter (де)сериализация происходит с помощью ключевых методов Serialize() и Deserialize().
Сохраним состояние нашего объекта SuperHero в виде SOAP сообщения:
//Сохраняем состояние объекта superHuman в SOAP формате
SoapFormatter soap = new SoapFormatter();
using(var fStream = new FileStream("./SuperHumanInfo.soap", FileMode.Create, FileAccess.Write, FileShare.None))
{
    soap.Serialize(fStream, superHuman);
}
В результате выполнения данного кода будет создан файл SuperHumanInfo.soap. Заглянув внутрь которого, можно увидеть XML-элементы, описывающие состояние нашего объекта superHuman с указателями ref, описывающими отношения между объектами в графе.
Вот небольшой кусок содержимого SuperHumanInfo.soap:
...
<superpower href="#ref-3"/>
<hasSecret>true</hasSecret>
<_x003C_isEvil_x003E_k__BackingField>false</_x003C_isEvil_x003E_k__BackingField>
<Human_x002B_name id="ref-4">Invisible Sharpshooter</Human_x002B_name>
...
Приватное поле hasSecret было также, как и в случае с BinaryFormatter, успешно сохранено.
Из двух механизмов сериализации: BinaryFormatter и SoapFormatter, рекомендуется использовать первый т.к. начиная с версии .NET 3.5, SoapFormatter считается устаревшим.

Сериализация в XML формат с помощью XmlSerializer
XmlSerializer не сохраняет приватные данные. Хотя это можно сделать, инкапсулировав такое поле в общедоступном свойстве:
private string name;
public string Name
{
    get { return name; }
    set { name = value; }
}
Также XmlSerializer не сохраняет точную информацию о типе (квалифицированное имя, имя сборки и т.д.), что делает его идеальным кандидатом когда необходимо сохранить объект для дальнейшего использования в другом языке программирования, а также на любой платформе, в любой операционной системе.
Сериализация с помощью XmlSerializer немного отличается от сериализации с помощью BinaryFormatter и SoapFormatter. XmlSerializer требует указания информации о типе, который нужно сериализовать. Также, если определялись конструкторы отличные от конструктора по умолчанию, то во избежание исключения InvalidOperationException, необходимо добавить конструктор по умолчанию.
А теперь сохраним, уже ставший нам родным объект типа SuperHero в формате XML:
//Сохраняем состояние объекта superHuman в XML формате
XmlSerializer xml =  new XmlSerializer(typeof(SuperHuman));
using (var fStream = new FileStream("./SuperHumanInfo.xml", FileMode.Create, FileAccess.Write, FileShare.None))
{
    xml.Serialize(fStream, superHuman);
}
Итогом сериализации будет сгенерированный XML файл со следующим содержимым:
<?xml version="1.0"?>
<SuperHuman xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <age>20</age>
  <Name>Invisible Sharpshooter</Name>
  <superpower>
    <isUnique>true</isUnique>
    <abilities>
      <string>sharp eye</string>
      <string>invisibility</string>
    </abilities>
  </superpower>
  <isEvil>false</isEvil>
</SuperHuman>
Состояние приватного поля hasSecret сохранить не удалось, в отличие от приватного поля name благодаря наличию общедоступного свойства Name.
Изначально XmlSerializer сохраняет данные объекта как XML элементы. С помощью ряда аттрибутов, которые есть в наличии у класса XmlSerializer, можно управлять генерацией итогового XML документа: сериализовать поле/свойство как XML аттрибут, сериализовать поле/свойство с определенным именем, сконструировать root элемент и т.д.

В заключение
Для сохранения состояния объекта можно воспользоваться одним из доступных в .NET механизмов сериализации:
- BinaryFormatter
- SoapFormatter
- XmlSerializer

Выбор механизма зависит от поставленной задачи. Если нужна скорость, хорошее сжатие данных и вы работает в пределах инфраструктуры CLI, используйте BinaryFormatter.
Для передачи же сложных структур, когда необходимо представить данные в читабельном формате, и вы не хотите зависеть от платформы, языка программирования и т.д используйте XmlSerializer.
Также можно воспользоваться и SoapFormatter, но как я сказал ранее, начиная с версии .NET 3.5 этот класс считается устаревшим (obsolete).

10 комментариев:

  1. просто, доступно, понятно) спасибо!)

    ОтветитьУдалить
    Ответы
    1. Старался писать, как для себя - то, как стало бы понятно мне, если бы не знал.

      Удалить
  2. огромное спасибо, очень признателен, все получилось.

    ОтветитьУдалить
  3. Дякую. Доступно і зрозуміло

    ОтветитьУдалить
  4. Добавлю, что для BinaryFormatter необходимо использовать модуль System.Runtime.Serialization.Formatters.Binary, для SoapFormatter подключить и использовать модуль System.Runtime.Serialization.Formatters.Soap, а для XmlSerializer использовать модуль System.Xml.Serialization.

    ОтветитьУдалить
  5. BinaryFormatter создает очень большого размеры файлы. Советую глянуть еще и protobuf.

    ОтветитьУдалить