Сериализация объектов в 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);
            }
Статья будет дополняться.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
2
29
6 лет назад
Отредактирован Doc
2
Я согласен что если есть зависимость от сторонних тулз - можно и подстроиться под формат. В основном правда очень часто единственной тулзой является сам движок игры. И свой сериалайз + парсинг пишется очень и очень быстро, особенно в хайлевел языках, где работа со строками уже налажена. К примеру взгляните на:
здесь функция split_into_words_and_quoted_pieces из 60 строк это весь токенайзер. Далее парсинг совершенно тривиален и добавлять новые части к нему ничего не стоит.
он определяет скобки как блок данных, все нормально читается.
Это ничего не дает, в формате куча визуального мусора и он слишком прост чтобы в него сериализовать более сложные данные, например список из объектов разных под-типов.
Когда в жсоне один линейный список он читается нормально. Как только данные усложняются формат становится не сильно более понятным чем бинарный.
0
9
6 лет назад
Отредактирован AsagiriGen
0
Как только данные усложняются формат становится не сильно более понятным чем бинарный.
Как и твой приведенный, разве нет? Визуальный мусор есть, но сам формат простой. У меня не было случаев, когда не разобрался при просмотре тестового сохранения или я не писал ничего сложного.
Мои причины использования json очень простые: она почти ничем не хуже твоего метода; часто можно не писать парсер, а использовать работающую либу; этот формат известен всем в отличие от своего персонального.
Но твою позицию со своим форматом пониманию.
0
14
5 лет назад
0
Не мог пройти мимо, не ответив на заблуждения вышеотписавшихся.
  1. сериализация вручную пилится в редких случаях, я за свои 14 лет опыта программирования пришел в выводу что юзать готовые решения - лучше, в 1ю очередь изза их сторонней поддержки и правки багов, плюс функицонала больше чем вы сами навелосипедите
  2. основная задача при сериализации с любыми решениями хоть готовыми хоть вашими - описать данные, пишите C# классы, и когда структуры классов и иераррхии вложенности усложняются - как раз выгоднее писать нормальные C# классы и отдать на откуп системе их перевод в текст JSON/XML или даж бинарный, а не самому писать как каждый подтип будет преобразовываться
  3. нет проблем сохранять полиморфные объекты в JSON, юзайте JsonDotNet с опцией чтобы писало $type при сериализации и соответсвенно нужный подкласс инстанциируется при десериализации
  4. при текстовом формате сериализации, что JSON что XML, не столь важен выбор, когда речь о читаемости итоговых сериализованных файлов - это редкое действие, к тому же открываете в текст редакторе, и при сериализации включаете PrettyPrint чтобы на 1 строке был 1 атрибут, а не все в 1 строку
0
9
5 лет назад
Отредактирован AsagiriGen
0
Перечитал комментарии. На данный момент мое дилетантское мнение таково, что json дает хорошую читаемость в простейших случаях + всякие библиотеки, чтоб не писать самому. Во всех остальных случаях, думается, своя сериализация будет лучшим решением, потому что:
  1. Вы понимаете, как работает ваша сериализация. И если будете ловить какие-то баги, то понимаете, как ее исправить, чего не скажешь о какой-то либе, начинку который вы не знаете.
  2. По производительности как конечное решение своя кастомная сериализация всегда лучше.
0
14
5 лет назад
Отредактирован Alex_Hell
0
Вы понимаете, как работает ваша сериализация. И если будете ловить какие-то баги, то понимаете, как ее исправить, чего не скажешь о какой-то либе, начинку который вы не знаете
только в готовых решениях багов как правило почти нет, или очень редкие, и при желании можно выбрать либу-конкурентов без этих багов если их править не будут.. а свой велосипед надо самому поддерживать. в том числе и баги фиксить, и функционал добавлять - всякие подтипы полиморфные в списке (как Doc указывал, а я сказал что они уже реализованы) много чего еще из-коробки есть
  1. производительность там где нужна - можно юзать protobuf или иже с ними бинарные.. текстовая десериализация в/из json не будет самописная быстрее чем готовые либы json, за счет чего? рефлексию если убрать даже - есть генераторы кода, которые без рефлексии компилят, и при этом будет чуть быстрей, но руками такое городить - долго, я имею ввиду свой генератор кода, где будет вида
output.writeTag("health", health);
output.writeHierarchyOfObject("target", target); где target спец подтип, и в него тоже вручную сериалайзить
так вот руками написанное такое всмысле без генератора - будет с рефлекскией - и медленно, а без рефлексии нужен свой велосипед - жесть будет
.. про генератор я имел ввиду что по CLASS декларации само сгенерит код того вида выше с writeTag которые руками не надо писать - и будет быстрее чем любой самописный елосипед, просто это доп требование - если надо - найдете сериалайзер даже json такой
\ protobuf - уже юзает такие же генераторы, без рефлексии
2
29
5 лет назад
2
Намеки на то, сколько времени занимает подобное дело - оверэстимейшн. Написание текстовой сериализации для объекта занимает минуты.
0
24
5 лет назад
0
Alex_Hell,
Не знаю как у вас в шарпах, а на джаве я пользовался встроенной в среду разработки возможностью выполнять перед компиляцией проекта код, получающий информацию об аннотациях в коде проекта - кастомный сериализатор-десериализатор с генерацией кода писался очень быстро на основе этого.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.