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

Добавлен , опубликован
В этой статье будет подробно разобрана сериализация/десериализация объектов, ее предназначение, форматы и случаи, где какой формат сериализации использовать.
В наше время нередко приходится сталкиваться с такими ситуациями, когда на запоминающем устройстве нужно сохранить необходимую информацию, где оная может принимать вид некоторых структур данных. Структуры данных могут быть представлены как в виде простых объектов с парой параметров, так и сложных в виде многочисленных иерархий объектов. При становлении вопроса о сохранении этих данных, у вас либо какая-то агрессия и зубы скрипят, либо вы вспоминаете то, что здесь было изложено и, по выполнении задачи, продолжаете радоваться жизни.
С этой проблемой призван справится механизм сериализации, где сериализация еть процесс преобразования какой-либо сущности в поток байтов. После преобразования мы можем этот поток байтов или записать на диск в необходимом формате или сохранить его временно в памяти. А при необходимости можно выполнить обратный процесс - десериализацию, то есть, получить из потока байтов ранее сохраненный объект и привести в изначальный вид.
Перечислю несколько распространенных форматов сериализации/десериализации, где каждый из перечисленных имеет свои преимущества.
Форматы и проведенный бенчмарк:
  • Xml-сериализация
| + При больших объемах данных* быстрее всех сериализуется и десериализуется

  • Json-сериализация
| + при малых объемах данных* быстрее всех сериализуется и десериализуется
| + исходный файл меньше весит при больших и малых объемах данных
Идеально подходит для кратковременной сериализации объектов Unity.

  • Бинарная-сериализация
| + нечитабельное содержимое, позволяющее скрыть информацию от сторонних глаз
| + поддерживает больше типов для де/сериализации
Подходит к долговременной сериализации объектов Unity, где подразумевается сокрытие данных от "взломщиков".

| большим объемом считается количество более 1500 объектов
| малым объемом считается количество менее 500 объектов

XML сериализация

XML сериализация сериализует только публичные поля и свойства
XML сериализация должна должна на этапе компиляции располагать информацией о типах, которые сериализует
Сериализуемые объекты должны иметь беспараметрический конструктор
Свойства с модификатором readonly не сериализуются
Для начала напишем класс-шаблон для сведений об игровом состоянии. Выглядеть он будет следующим образом:
        public class GameState
        {
            public int Money { get; set; }
            public int Lives { get; set; }
        }
И метод для сериализации наших данных.
            GameState state = new GameState() // создаем объект с данными, базируясь на классе-шаблоне
            {
                Money = 1488,
                Lives = 228
            };

            XmlSerializer serializer = new XmlSerializer(typeof(GameState)); // создаем сериализатор и сообщаем ему о том, какой тип сериализуем
            using (TextWriter writer = new StreamWriter(@"C:\Users\Msey\Desktop\GameState.xml")) // если вкратце, то здесь мы создаем модуль, позволяющий записывать символы по указанной директории
            {
                serializer.Serialize(writer, state); // сериализуем данные
            }
Выходные данные будут выглядеть следующим образом:
<?xml version="1.0" encoding="utf-8"?>
<GameState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Money>1488</Money>
  <Lives>228</Lives>
</GameState>
Теперь отредактируем наш класс-шаблон так, чтобы убедиться, что приватные поля не сериализуются и что с непараметрическим конструктором все работает в полной мере.
        public class GameState
        {
            public int money;
            public int lives;
            private int weed;

            public GameState()
            {
                money = 1111;
                lives = 2222;
                weed  = 3333; // не будет сериализоваться, тк поле weed с модификатором доступа private
            }
        }
Результат очевиден:
<?xml version="1.0" encoding="utf-8"?>
<GameState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <money>1111</money>
  <lives>2222</lives>
</GameState>
Если мы заменим пустой конструктор на конструктор с параметрами, то в процессе выполнения программы мы получим ошибку выполнения на этапе создания сериализатора:
            public GameState(int i =1111, int j =2222) // так нельзя - конструктор с параметрами
            {
                money = i;
                lives = j;
            }



            public GameState(int i , int j) // так тоже нельзя - конструктор продолжает быть параметрическим (Ваш кэп ©)
            {
                money = i;
                lives = j;
            }
Рассмотрим распространенные атрибуты XML-сериализации:
  • [XmlElement]: поле будет сериализовано в качестве элемента
  • [XmlAttribute]: поле будет сериализовано в качестве атрибута
  • [XmlIgnore]: поле будет пропущено во время сериализации
  • [XmlRoot]: задает корневой элемент при сериализации
Рассмотрим атрибут XmlElement:
        public class GameState
        {
            [XmlElement("no_money")] 
            public int money; // независимо от того, как называется поле, атрибут XmlElement указывает, что представляет элемент строкой ниже и сериализует/десериализует его под именем, указанном в этом атрибуте
            [XmlElement("no_lives")]
            public int lives;

            public GameState()
            {
                money = 1111;
                lives = 2222;
            }
        }
Результат:
<?xml version="1.0" encoding="utf-8"?>
<GameState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <no_money>1111</no_money>
  <no_lives>2222</no_lives>
</GameState>
Также атрибут [XmlElement("no_money")] можно записать в виде [XmlElement(ElementName ="no_money")]. Отличие заключается в том, что в атрибуте [XmlElement(ElementName ="no_money")] можно записать несколько параметров:
[XmlElement(ElementName ="no_money", Namespace = "nmspc")]
Здесь мы добавили пространство имен, и тогда данный элемент в файле принимает следующий вид:
<no_money xmlns="nmspc">1111</no_money>
Теперь обратим внимание на поведение при аттрибуте XmlAttribute:
        public class GameState
        {
            [XmlAttribute("MoneyAttribute")]
            public int money;

            [XmlAttribute("LivesAttribute")]
            public int lives;

            public GameState()
            {
                money = 2222;
                lives = 2222;
            }
        }
Результат:
<?xml version="1.0" encoding="utf-8"?>
<GameState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" MoneyAttribute="2222" LivesAttribute="2222" />
Аттрибут XmlIgnore позволяет игнорировать поле во время сериализации. Заменим класс:
        public class GameState
        {
            [XmlIgnore]
            public int money;
            [XmlIgnore]
            public int lives;

            public GameState()
            {
                money = 1111;
                lives = 2222;
            }
        }
И наблюдаем результат:
<?xml version="1.0" encoding="utf-8"?>
<GameState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
В сгенерированном XML файле отсутствуют элементы.
Атрибут XmlRoot применяется в качестве указания корневого каталога его содержимого. Корневыми элементами могут быть структуры, перечисления, классы и интерфейсы.
        [XmlRoot("Root_GameState")]
        public class GameState
        {
            public int money;
            public int lives;

            public GameState()
            {
                money = 1111;
                lives = 2222;
            }
        }
Результат:
<?xml version="1.0" encoding="utf-8"?>
<Root_GameState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <money>1111</money>
  <lives>2222</lives>
</Root_GameState>
Десериализация для класса GameState не слишком сильно отличается от сериализации:
            GameState state;

            XmlSerializer deserializer = new XmlSerializer(typeof(GameState));

            using (TextReader reader = new StreamReader(@"C:\Users\Msey\Desktop\GameState.xml"))
            {
                state = (GameState) deserializer.Deserialize(reader);
            }

Json сериализация

Сериализация в json имеет схожий с XML процесс сериализации:
            GameState state = new GameState();

            JsonSerializer serializer = new JsonSerializer();

            using (StreamWriter sw = new StreamWriter(@"C:\Users\Msey\Desktop\GameState.json"))
            using (JsonWriter writer = new JsonTextWriter(sw))
            {
                serializer.Serialize(writer, state);
            }
Перечислю, пожалуй, основные атрибуты для сериализации в json, которые вам могут пригодиться в дальнейшем:
  • [JsonObjectAttribute] - атрибут, который используется для задания поведения класса при сериализации
  • [JsonPropertyAttribute] - атрибут, который используется для задания поведения свойств и полей при сериализации
  • [JsonIgnore] - атрибут, позволяющий игнорировать поле или свойство при сериализации
Десериализуем теперь json обратно в объект:
            GameState state;

            JsonSerializer serializer = new JsonSerializer();

            using (StreamReader sw = new StreamReader(@"C:\Users\Msey\Desktop\GameState.json"))
            using (JsonReader writer = new JsonTextReader(sw))
            {
                state = (GameState) serializer.Deserialize(writer);               
            }

Бинарная сериализация

Процесс бинарной сериализации выглядит так:
            GameState state = new GameState();

            BinaryFormatter formatter = new BinaryFormatter();

            using (FileStream stream = new FileStream(@"C:\Users\Msey\Desktop\GameState.dat", FileMode.Create))
            {
                formatter.Serialize(stream, state);
            }
Однако стоит заметить, что конструктор BinaryFormatter не принимает тип сериализуемого объекта, а это значит, что на этапе компиляции formatter не будет ничего знать о нашем классе (типе) с игровым состоянием и в процессе выполнения выдаст ошибку, поэтому добавим к нему атрибут [Serializable]:
        [Serializable]
        public class GameState
        {
            public int money;
            public int lives;

            public GameState()
            {
                money = 1111;
                lives = 2222;
            }
        }
В случае с десериализацией можно пренебречь атрибутом [Serializable], однако я рекомендую этот атрибут использовать всегда для сериализуемых объектов как для читаемости, так и "кросссериализуемости".
Процесс десериализации:
            GameState state;

            BinaryFormatter formatter = new BinaryFormatter();

            using (FileStream stream = new FileStream("C:\Users\Msey\Desktop\GameState.dat", FileMode.Open))
            {
                state = (GameState)formatter.Deserialize(stream);
            }
Статья будет дополняться.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
29
Если честно, то сравнение сериализации представлено так-себе. 1.5к объектов не показатель. Самый важный показатель сериализации не рассмотрен.
29
1.5к объектов не показатель
Msey
более 1500
Самый важный показатель сериализации не рассмотрен
Ну так поведай нам.
29
Ну так поведай нам.
Обратная-совместимость данных. Что делать, когда ты уже в продакшене, а надо расширить или урезать данные, сериализованные в формате X
29
Все это медленнее (за исключением бинарной сериализации) и менее читаемо чем просто ручная запись файлов в плейн-текст формате уровня:
game_state
money 999.99
lives 12
Текст разбивается по пробелам и лайнбрейкам и ручками пишется recursive descent parser (простейшая вещь с которой справится и школьник). Подобный формат легко править и читать даже без наличия продвинутого редактора, он нормально будет смотреться в гит диффах. При желании легчайшим образом добавляется собственное версионирование, к примеру:
game_state
money 999.99 Since 1.2
lives 12
Одумайтесь. Жсон и хмл совершенно нечитаемые форматы при больших объемах древовидных данных.
24
Doc, Согласен. Но на самом деле сильно зависит от юзкейсов. Например, собрать прототип на готовой JSON сериализации будет быстрее, если своей либы еще нет готовой - запилить потом другой алгоритм сериализации никто не мешает, когда понадобится и будет дополнительно время на это.
Сам пользуюсь JSON-ом для хранения статичных данных в своем основном проекте т.к. руки не дошли пилить кастомную сериализацию, а анрил из коробки понимает превращение таблиц данных в JSON и обратно и, соответственно, в наличии визуальный редактор и другие плюшки, которые иначе пришлось бы пилить вручную либо с нуля либо допиливать к существующим инструментам. Правда руками я их в итоге трогаю довольно редко, а ингейм сохранения всеравно в бинарниках.
9
json рулит: по структуре проще xml, а сама структура более понятная чем прямой текст. Про читаемость - открыл в Notepad++ и поехали, он определяет скобки как блок данных, все нормально читается.
29
Я согласен что если есть зависимость от сторонних тулз - можно и подстроиться под формат. В основном правда очень часто единственной тулзой является сам движок игры. И свой сериалайз + парсинг пишется очень и очень быстро, особенно в хайлевел языках, где работа со строками уже налажена. К примеру взгляните на:
здесь функция split_into_words_and_quoted_pieces из 60 строк это весь токенайзер. Далее парсинг совершенно тривиален и добавлять новые части к нему ничего не стоит.
он определяет скобки как блок данных, все нормально читается.
Это ничего не дает, в формате куча визуального мусора и он слишком прост чтобы в него сериализовать более сложные данные, например список из объектов разных под-типов.
Когда в жсоне один линейный список он читается нормально. Как только данные усложняются формат становится не сильно более понятным чем бинарный.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.