![]() |
Сериализация данных в языке C# |
![]() |
Принципиальные подходы к сериализации информации
В процессе работы программы, её переменные хранятся в произвольных участках оперативной памяти компьютера. Значимые типы, а так же данные массивов и строк занимают непрерывные участки, в то время как составные ссылочные типы данных (например, классы, содержащие в качестве полей другие классы) представляют собой сложную древовидную структуру.
Сериализацией называется процесс трансформации значения произвольного типа
данных в непрерывную последовательность байт (byte[]
). Этот процесс должен
происходить без потери информации, чтобы полученный массив байт мог бы быть
десериализован обратно в значение исходного типа.
Наиболее распространёнными сценариями, в которых требуется применять сериализацию являются:
- Сохранение данных приложения в файл. Например, различные текстовые и графические редакторы имеют возможность сохранять и загружать “файл проекта” с диска компьютера. Технически, процесс сохранения такого файла является сериализацией.
- Передача информации по сети. В предыдущей работе было показано, что интерфейс сокетов позволяет передавать данные только в виде массива байт. Как следствие, отправляющей стороне необходимо производить сериализацию посылаемой информации, а принимающей стороне - её десериализацию.
Существующие алгоритмы сериализации/десериализации можно разделить на две группы:
- Текстовая сериализация. Поток байт, создаваемый в результате текстовой сериализации представляет собой читаемый текст, оформленный в соответствии с определёнными синтаксическими правилами. К алгоритмам текстовой сериализации относятся JSON и XML.
- Двоичная (бинарная) сериализация. Массив байт, полученный при двоичной сериализации может содержать непечатаемые символы. Такие алгоритмы, чаще всего, уникальны для каждого конкретного языка программирования, однако существует несколько универсальных стандартов двоичной сериализации: CBOR, ASN.1.
Преимущество текстовой сериализации состоит, во-первых, в возможности редактировать сериализованные данные с помощью простейшего текстового редактора, а во-вторых - в её интероперабельности. Благодаря тому, что сериализованные данные представляют собой обычный текст, и практически любой язык программирования имеет в своём составе функции для работы со строками, становится сравнительно несложным делом десериализовать данные даже с помощью какого-либо экзотического языка программирования. Популярность JSON и XML во многом обусловлена именно тем, что информация, сериализованная посредством одного языка программирования, может быть десериализована с помощью другого языка программирования.
Главное преимущество двоичной сериализации заключается в её эффективности. Двоичные форматы хранения данных, с одной стороны, более компакты, а с другой - их обработка занимает гораздо меньше времени.
Недостатки обоих подходов к сериализации вытекают из их преимуществ. Текстовая сериализация является избыточной, и, как следствие, требует большего объёма памяти при хранении и вычислительных ресурсов при обработке. Двоичная же сериализация является неуниверсальной, и зачастую требует использования одного и того же языка программирования на сериализующей и десериализующей стороне.
Понятие о механизме рефлексии
В прошлой работе мы рассмотрели наиболее примитивный подход к сериализации базовых типов данных: чисел и строк. Не смотря на свою простоту, он не подходит для работы со сложными типами данных, т.к. требует написания большого количества рутинного (сл. boilerplate) кода. Рассмотрим следующий класс:
class Книга
{
public string Название;
public List<string> Авторы;
public int КоличествоСтраниц;
public string Издательство;
}
Объём кода, необходимого для сериализации объекта типа Книга
, будет расти
по мере добавления новых полей в класс. Вместе с тем, изменения кода сериализации
должны производиться параллельно с изменениями кода десериализации. В предыдущей
работе было показано, что единственная ошибка в порядке десериализации приводит
к некорректной десериализации всех последующих данных.
Можно представить более удобный и безопасный способ сериализации. Допустим, что
язык C# предоставляет программисту метод .Fields()
, который возвращает
коллекцию полей соответствующего класса. Используя этот метод, можно было бы
написать универсальный код для сериализации объектов класса Книга
:
class Книга
{
public string Название;
public List<string> Авторы;
public int КоличествоСтраниц;
public string Издательство;
public byte[] Serialize()
{
foreach (Field f in Fields())
{
byte[] serialized = {};
if (f.Type == "int")
= serialized.Concat(BitConverter.GetBytes(f.Value)).ToArray();
serialized if (f.Type == "string")
= serialized.Concat(Encoding.Unicode.GetBytes(f.Value)).ToArray();
serialized // ...
return serialized;
}
}
}
В примере выше метод .Fields()
возвращает список полей класса Книга
, а
воображаемый класс Field
содержит информацию о каждом отдельном поле. Например,
для первого поля класса Книга
, объект Field f
должен был бы содержать
значение "string"
в f.Type
и непосредственно название книги в f.Value
.
Применяя такой подход, программисту больше нет надобности вручную повторять код
сериализации/десериализации для каждого поля класса - все поля обрабатываются
единообразно в цикле. Необходимо лишь для каждого нового типа поля написать
соответствующую ветвь оператора if
:
if (f.Type == "double")
// ...
Механизм, позволяющий коду “взглянуть на себя” называется рефлексией, и в
действительности присутствует в языке C#. Более того, решая задачи сериализации,
программисту, в большинстве случаев, даже не требуется напрямую использовать
этот механизм, как и реализовывать метод .Serialize()
из примера выше. Вместо
этого, дело сводится к вызову встроенного метода .Serialize()
, соответствующего
выбранному алгоритму сериализации
Сериализация JSON
Синтаксис нотации JSON чрезвычайно прост, что и обуславливает его популярность
среди различных языков программирования. Приведём пример того, как будет
выглядеть сериализованный объект класса Книга
:
{
"Название": "C# для чайников",
"Авторы": ["Мюллер Джон Поль", "Семпф Билл"],
"КоличествоСтраниц": 608,
"Издательство": "Диалектика"
}
Объект JSON обрамляется символами {
и }
и состоит из произвольного числа
полей. Каждое поле имеет вид ключ: значение
, где ключ
- произвольная строка.
Значениями в JSON являются:
- Целые числа и числа с плавающей точкой (
1
,-1
,1.0
); - Строки (
"asdfg"
); - Массивы значений (
[123, 456]
); - Булевы значения (
true
,false
); - Другие объекты JSON (
{ ... }
); - Значение
null
.
Наконец, рассмотрим код, сериализующий и десериализующий JSON в C#. Необходимые
для этого классы находятся в пространстве имён System.Text.Json
:
using System.Text.Json;
using System.Text.Json.Serialization;
Чтобы дать программисту возможность определять, какие члены классы должны
подвергаться сериализации, а какие - нет, используется следующее правило:
сериализуются только свойства, а не обычные поля. Как следствие, нам
необходимо слегка изменить объявление класса Книга
:
class Книга
{
public string Название {get; set; }
public List<string> Авторы {get; set; }
public int КоличествоСтраниц {get; set; }
public string Издательство {get; set; }
}
Теперь произведём сериализацию и десериализацию:
// Создадим объект, который хотим сериализовать
= new Книга();
Книга к .Название = "C# для чайников";
к.Авторы = new List<string>({"Мюллер Джон Поль", "Семпф Билл"});
к.КоличествоСтраниц = 608;
к.Издательство = "Диалектика";
к
// Сериализуем объект в JSON-строку
string serializedStr = JsonSerializer.Serialize(к);
// Сериализуем объект сразу в массив байт
byte[] serializedBytes = JsonSerializer.SerializeToUtf8Bytes(к);
// Десериализуем JSON из строки
= JsonSerializer.Deserialize<Книга>(serializedStr);
Книга к2 // Десериализуем JSON из массива байт
= JsonSerializer.Deserialize<Книга>(serializedbytes); Книга к3
Отметим, что метод .Deserialize<T>()
параметризуется типом T
, в который
производится десериализация. Разумеется, этот тип должен совпадать с типом
переменной, которая была передана в .Serialize()
.
Контрольные вопросы
- Виды алгоритмов сериализации, их преимущества и недостатки.
- В чём фундаментальная проблема “ручного” подхода к сериализации, описанного в предыдущей лабораторной работе?
- Что такое рефлексия в языках программирования и зачем она нужна?
- Как будет выглядеть результат сериализации в JSON объекта типа
Dictionary<string, int>
?
Задание на лабораторную работу
Модифицируйте клиентское и серверное приложения, разработанные в предыдущей
лабораторной работе, таким образом, чтобы операции сериализации и десериализации
производились при помощи JsonSerializer
. Убедитесь, что после модификации
приложения продолжают функционировать корректно. В результате выполнения этой
работы, объём кода обоих проектов должен уменьшиться.
