Unity 3D: Сериализация объектов в 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);
            }
Статья будет дополняться.

Просмотров: 690

» Лучшие комментарии


alexprey #1 - 4 месяца назад -3
Если честно, то сравнение сериализации представлено так-себе. 1.5к объектов не показатель. Самый важный показатель сериализации не рассмотрен.
Msey #2 - 4 месяца назад (отредактировано ) 0
1.5к объектов не показатель
Msey
более 1500
Самый важный показатель сериализации не рассмотрен
Ну так поведай нам.
alexprey #3 - 4 месяца назад 0
Ну так поведай нам.
Обратная-совместимость данных. Что делать, когда ты уже в продакшене, а надо расширить или урезать данные, сериализованные в формате X
Doc #4 - 4 месяца назад (отредактировано ) 4
Все это медленнее (за исключением бинарной сериализации) и менее читаемо чем просто ручная запись файлов в плейн-текст формате уровня:
game_state
money 999.99
lives 12
Текст разбивается по пробелам и лайнбрейкам и ручками пишется recursive descent parser (простейшая вещь с которой справится и школьник). Подобный формат легко править и читать даже без наличия продвинутого редактора, он нормально будет смотреться в гит диффах. При желании легчайшим образом добавляется собственное версионирование, к примеру:
game_state
money 999.99 @Since 1.2
lives 12
Одумайтесь. Жсон и хмл совершенно нечитаемые форматы при больших объемах древовидных данных.
prog #5 - 4 месяца назад 0
Doc, Согласен. Но на самом деле сильно зависит от юзкейсов. Например, собрать прототип на готовой JSON сериализации будет быстрее, если своей либы еще нет готовой - запилить потом другой алгоритм сериализации никто не мешает, когда понадобится и будет дополнительно время на это.
Сам пользуюсь JSON-ом для хранения статичных данных в своем основном проекте т.к. руки не дошли пилить кастомную сериализацию, а анрил из коробки понимает превращение таблиц данных в JSON и обратно и, соответственно, в наличии визуальный редактор и другие плюшки, которые иначе пришлось бы пилить вручную либо с нуля либо допиливать к существующим инструментам. Правда руками я их в итоге трогаю довольно редко, а ингейм сохранения всеравно в бинарниках.
GeneralElConsul #6 - 4 месяца назад 0
json рулит: по структуре проще xml, а сама структура более понятная чем прямой текст. Про читаемость - открыл в Notepad++ и поехали, он определяет скобки как блок данных, все нормально читается.
Doc #7 - 4 месяца назад (отредактировано ) 2
Я согласен что если есть зависимость от сторонних тулз - можно и подстроиться под формат. В основном правда очень часто единственной тулзой является сам движок игры. И свой сериалайз + парсинг пишется очень и очень быстро, особенно в хайлевел языках, где работа со строками уже налажена. К примеру взгляните на:
здесь функция split_into_words_and_quoted_pieces из 60 строк это весь токенайзер. Далее парсинг совершенно тривиален и добавлять новые части к нему ничего не стоит.
он определяет скобки как блок данных, все нормально читается.
Это ничего не дает, в формате куча визуального мусора и он слишком прост чтобы в него сериализовать более сложные данные, например список из объектов разных под-типов.
Когда в жсоне один линейный список он читается нормально. Как только данные усложняются формат становится не сильно более понятным чем бинарный.
GeneralElConsul #8 - 4 месяца назад (отредактировано ) 0
Как только данные усложняются формат становится не сильно более понятным чем бинарный.
Как и твой приведенный, разве нет? Визуальный мусор есть, но сам формат простой. У меня не было случаев, когда не разобрался при просмотре тестового сохранения или я не писал ничего сложного.
Мои причины использования json очень простые: она почти ничем не хуже твоего метода; часто можно не писать парсер, а использовать работающую либу; этот формат известен всем в отличие от своего персонального.
Но твою позицию со своим форматом пониманию.