Что такое сериализация в c
Перейти к содержимому

Что такое сериализация в c

  • автор:

Сериализация

Ранее мы посмотрели, как сохранять информацию в текстовые файлы, а также затронули сохранение несложных структур в бинарные данные. Но нередко подобных механизмов оказывается недостаточно особенно для сохранения сложных объектов. С этой проблемой призван справится механизм сериализации. Сериализация представляет процесс преобразования какого-либо объекта в поток байтов. После преобразования мы можем этот поток байтов или записать на диск или сохранить его временно в памяти. А при необходимости можно выполнить обратный процесс — десериализацию , то есть получить из потока байтов ранее сохраненный объект.

Атрибут Serializable

Чтобы объект определенного класса можно было сериализовать, надо этот класс пометить атрибутом Serializable :

При отстутствии данного атрибута объект Person не сможет быть сериализован, и при попытке сериализации будет выброшено исключение SerializationException .

Сериализация применяется к свойствам и полям класса. Если мы не хотим, чтобы какое-то поле класса сериализовалось, то мы его помечаем атрибутом NonSerialized :

При наследовании подобного класса, следует учитывать, что атрибут Serializable автоматически не наследуется. И если мы хотим, чтобы производный класс также мог бы быть сериализован, то опять же мы применяем к нему атрибут:

Формат сериализации

Хотя сериализация представляет собой преобразование объекта в некоторый набор байтов, но в действительности только бинарным форматом она не ограничивается. Итак, в .NET можно использовать следующие форматы:

Для каждого формата предусмотрен свой класс: для сериализации в бинарный формат — класс BinaryFormatter , для формата SOAP — класс SoapFormatter , для xml — XmlSerializer , для json — DataContractJsonSerializer .

Для классов BinaryFormatter и SoapFormatter сам функционал сериализации определен в интерфейсе IFormatter :

Хотя классы BinaryFormatter и SoapFormatter по-разному реализуют данный интерфейс, но общий функционал будет тот же: для сериализации будет использоваться метод Serialize , который в качестве параметров принимает поток, куда помещает сериализованные данные (например, бинарный файл), и объект, который надо сериализовать. А для десериализации будет применяться метод Deserialize , который в качестве параметра принимает поток с сериализованными данными.

Класс XmlSerializer не реализует интерфейс IFormatter и по функциональности в целом несколько отличается от BinaryFormatter и SoapFormatter, но и он также предоставляет для сериализации метод Serialize, а для десериализации Deserialize. И в этом плане очень легко при необходимости перейти от одного способа сериализации к другому.

Сериализация и десериализация данных в C#

Сериализация — это механизм преобразования объекта в поток байтов или текста. Deserialization берет ряд байтов или плоский файл и преобразует его в объект.

Почему требуется сериализация данных?

При создании приложений вам может потребоваться обмен данными с другими приложениями.
При отправке данных в веб-службу или через сетевой поток сначала необходимо преобразовать данные в плоскую или двоичную форму.
Когда вы получаете данные, вам необходимо преобразовать плоские или двоичные данные в объекты, с которыми вы хотите работать. Этот процесс называется сериализации и десериализации.

Сериализация сериализует только данные, хранящиеся в объекте. Методы не сериализованы.
Когда вы десериализуете объект, вам нужен доступ к первоначальному определению класса, или вы получите объект, который хранит данные только.
Вы можете создать пользовательский объект передачи данных (DTO), чтобы оптимизировать объем данных, которые вы должны сериализовать.

.NET Framework предоставляет пространства имен System.Runtime.Serialization и System.Xml.Serialization, которые помогут с сериализацией и десериализацией вашего объекта.

.NET Framework предоставляет три механизма сериализации, которые вы можете использовать по умолчанию:

  • XmlSerializer
  • DataContractSerializer
  • BinaryFormatter

Использование XmlSerializer

XmlSerializer был создан для поддержки обмена сообщениями протокола Simple Object Access Protocol (SOAP). SOAP — это протокол для обмена информацией с веб-службами. SOAP использует XML в качестве формата сообщений. XML не зависит от среды и читается как людьми, так и машинами.

Давайте посмотрим на приведенный ниже пример по сериализации объекта с помощью XmlSerializer.

В приведенном выше примере класс Person помечен Serializable. Все члены этого типа автоматически сериализуются, если они не откажутся.

В XmlSerializer вам необходимо пометить ваши типы с помощью атрибута [Serializable]. Это сообщает .NET Framework, что ваш тип должен быть сериализуемым. Если сериализация невозможна, вы получите исключение во время выполнения.

Вы можете настроить, как XmlSerializer сериализует ваш тип с помощью атрибутов.
Эти атрибуты определены в пространстве имен System.Xml.Serialization.
Важными атрибутами являются:
1. XmlIgnore
2. XmlAttribute
3. XmlElement
4. XmlArray
5. XmlArrayItem
По умолчанию каждый член сериализуется как элемент XmlElement

Использование атрибутов XML для настройки сериализации

Использование двоичной сериализации

XmlSerializer выводит текст, читаемый человеком. Вы можете открыть его в Notepad.But, используя двоичный формат, вы получите меньший результат.
Использование двоичной сериализации аналогично XmlSerializer.
Вам нужно отметить элемент с помощью параметра SerializableAttribute, а затем вы используете экземпляр двоичного сериализатора для сериализации объекта. Рамка .net обеспечивает System.Runtime.Serialization и System.Runtime.Serialization.Formatters.Binary пространства имен для поддержки двоичной сериализации.

В Binary Serialization частные поля по умолчанию сериализуются. Во время десериализации конструкторы не выполняются.
вы можете запретить сериализацию полей с использованием атрибута [NonSerialized], такого как XmlSerialization.

Если XMLserializer не может найти определенное поле, он не будет генерировать исключение; он просто установит свойство по умолчанию.
Но в двоичной сериализации она генерирует исключение. Таким образом, вы можете использовать OptionFieldAttribute, чтобы убедиться, что двоичный сериализатор знает, что поле добавлено в более позднюю версию.

Вы можете влиять на процесс сериализации и десериализации, используя следующие четыре атрибута:
1. OnDeserializedAttribute
2. OnDeserializingAttribute
3. OnSerializedAttribute
4. OnSerializingAttribute.
Вы можете добавить эти атрибуты к методам внутри вашего класса, которые принимают StreamingContext в качестве параметра.

Давайте рассмотрим ниже код о том, как добавить эти атрибуты в метод:

Сериализованный объект может предоставлять конфиденциальные данные, чувствительные к безопасности. Каждый, у кого есть разрешения на десериализацию файла, может получить доступ к вашим конфиденциальным данным. Если у вас класс чувствительный, вы должны реализовать интерфейс ISerializable.

Использование DataContractSerializer

DataContract Serialization в основном используется с WCF.

Серийный анализатор данных используется WCF для сериализации ваших объектов в XML или JSON.
Основное различие заключается в том, что вместо DataContractAttribute вы используете SerializableAttribute.
Другим важным отличием является то, что члены не сериализуются по умолчанию. Вы должны явно отмечать их атрибутом DataMember.

Давайте рассмотрим пример использования контракта Data & DataContract Serialization.

Использование DataContract

Использование JSON

JSON очень полезен при отправке небольших объемов данных между веб-сервером и клиентом с использованием асинхронного JavaScript и XML (AJAX).

Name already in use

docs / docs / csharp / programming-guide / concepts / serialization / index.md

  • Go to file T
  • Go to line L
  • Copy path
  • Copy permalink
  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file. Its main purpose is to save the state of an object in order to be able to recreate it when needed. The reverse process is called deserialization.

How serialization works

This illustration shows the overall process of serialization:

The object is serialized to a stream that carries the data. The stream may also have information about the object’s type, such as its version, culture, and assembly name. From that stream, the object can be stored in a database, a file, or memory.

Uses for serialization

Serialization allows the developer to save the state of an object and re-create it as needed, providing storage of objects as well as data exchange. Through serialization, a developer can perform actions such as:

  • Sending the object to a remote application by using a web service
  • Passing an object from one domain to another
  • Passing an object through a firewall as a JSON or XML string
  • Maintaining security or user-specific information across applications

The xref:System.Text.Json namespace contains classes for JavaScript Object Notation (JSON) serialization and deserialization. JSON is an open standard that is commonly used for sharing data across the web.

JSON serialization serializes the public properties of an object into a string, byte array, or stream that conforms to the RFC 8259 JSON specification. To control the way xref:System.Text.Json.JsonSerializer serializes or deserializes an instance of the class, you can use one or more of the following approaches:

  • Use a xref:System.Text.Json.JsonSerializerOptions object
  • Apply attributes from the xref:System.Text.Json.Serialization namespace to classes or properties

Binary and XML serialization

The xref:System.Runtime.Serialization namespace contains classes for binary and XML serialization and deserialization.

Binary serialization uses binary encoding to produce compact serialization for uses such as storage or socket-based network streams. In binary serialization, all members, even members that are read-only, are serialized, and performance is enhanced.

XML serialization serializes the public fields and properties of an object, or the parameters and return values of methods, into an XML stream that conforms to a specific XML Schema definition language (XSD) document. XML serialization results in strongly typed classes with public properties and fields that are converted to XML. xref:System.Xml.Serialization contains classes for serializing and deserializing XML. You apply attributes to classes and class members to control the way the xref:System.Xml.Serialization.XmlSerializer serializes or deserializes an instance of the class.

Making an object serializable

For binary or XML serialization, you need:

  • The object to serialize
  • A stream to contain the serialized object
  • A xref:System.Runtime.Serialization.Formatter?displayProperty=fullName instance

Apply the xref:System.SerializableAttribute attribute to a type to indicate that instances of the type can be serialized. An exception is thrown if you attempt to serialize but the type doesn’t have the xref:System.SerializableAttribute attribute.

To prevent a field from being serialized, apply the xref:System.NonSerializedAttribute attribute. If a field of a serializable type contains a pointer, a handle, or some other data structure that is specific to a particular environment, and the field cannot be meaningfully reconstituted in a different environment, then you may want to make it nonserializable.

If a serialized class contains references to objects of other classes that are marked xref:System.SerializableAttribute, those objects will also be serialized.

Basic and custom serialization

Binary and XML serialization can be performed in two ways, basic and custom.

Basic serialization uses .NET to automatically serialize the object. The only requirement is that the class has the xref:System.SerializableAttribute attribute applied. The xref:System.NonSerializedAttribute can be used to keep specific fields from being serialized.

When you use basic serialization, the versioning of objects may create problems. You would use custom serialization when versioning issues are important. Basic serialization is the easiest way to perform serialization, but it does not provide much control over the process.

In custom serialization, you can specify exactly which objects will be serialized and how it will be done. The class must be marked xref:System.SerializableAttribute and implement the xref:System.Runtime.Serialization.ISerializable interface. If you want your object to be deserialized in a custom manner as well, use a custom constructor.

Designer serialization is a special form of serialization that involves the kind of object persistence associated with development tools. Designer serialization is the process of converting an object graph into a source file that can later be used to recover the object graph. A source file can contain code, markup, or even SQL table information.

System.Text.Json overview
Shows how to get the System.Text.Json library.

How to serialize and deserialize JSON in .NET
Shows how to read and write object data to and from JSON using the xref:System.Text.Json.JsonSerializer class.

Walkthrough: Persisting an Object in Visual Studio (C#)
Demonstrates how serialization can be used to persist an object’s data between instances, allowing you to store values and retrieve them the next time the object is instantiated.

Сериализация в C++

В данной статье речь пойдет об автоматизации процесса сериализации в C++. В начале будут рассмотрены базовые механизмы, позволяющие упростить чтение/запись данных в потоки ввода-вывода, после чего будет дано описание примитивной системы генерации кода на основе libclang. Ссылка на репозиторий с демонстрационным вариантом библиотеки расположена в конце статьи.

На ruSO периодически встречаются вопросы, касающиеся сериализации данных в C++, иногда эти вопросы носят общий характер, когда TC в принципе не знает, с чего начать, иногда — это вопросы, описывающие конкретную проблему. Цель написания данной статьи заключается в кратком изложении одного из возможных способов имплементации сериализации в C++, которое позволит проследить этапы построения системы с начальных шагов до некоторого логического завершения, когда данной системой уже можно будет пользоваться на практике.

1. Начальные сведения

В данной статье будет использоваться бинарный формат данных, структура которых определяется на основании типов сериализуемых объектов. Такой подход избавляет нас от использования сторонних библиотек, ограничиваясь лишь теми средствами, которые предоставляет стандартная библиотека C++.

Так как процесс сериализации заключается в преобразовании состояния объекта в поток байтов, который, очевидно, должен сопровождаться операциями записи, последние будут использоваться вместо термина “сериализация” при описании низкоуровневых деталей. Аналогично для чтения/десериализации.

Для сокращения объема статьи будут приводиться только примеры сериализации объектов (за исключением тех случаев, когда десериализация содержит некоторые детали, о которых стоит упомянуть). Полный код можно найти в вышеупомянутом репозитории.

2. Поддерживаемые типы

Вначале стоит определиться с типами, которые мы планируем поддерживать — от этого напрямую зависит то, как будет реализована библиотека.

Например, если выбор ограничивается фундаментальными типами C++, то будет достаточно шаблона функции (который представляет собой семейство функций для работы со значениями целочисленных типов) и его явных специализаций. Первичный шаблон (используется для типов std::int32_t, std::uint16_t и т.д.):

Замечание: если полученные в процессе сериализации данные планируется передавать между машинами с разными порядками байтов, необходимо, например, конвертировать значение из локального порядка байтов в сетевой, а затем выполнить обратную операцию на удаленной машине, таким образом, внесение изменений будет необходимо как для функции записи данных в поток вывода, так и для функции чтения из потока ввода.

Специализация для bool:

Данный подход определяет следующее правило: если значение типа T может быть представлено в виде последовательности байт длинной sizeof(T), для него может быть использовано определение первичного шаблона, в противном случае — требуется определить специализацию. Данное требование может быть продиктовано особенностями представления объекта типа T в памяти.

Рассмотрим контейнер std::string: очевидно, что мы не можем взять адрес объекта указанного типа, привести его к указателю на char и записать в поток вывода — значит, нам требуется специализация:

Здесь необходимо сделать два важных замечания:

  1. В поток вывода записывается не только содержимое строки, но и ее размер.
  2. Приведение std::string::size_type к типу std::uint32_t. В данном случае стоить обратить внимание не на размер целевого типа, а на то, что он — фиксированной длины. Такое приведение позволит избежать проблем в случае, например, если данные передаются по сети между машинами у которых отличается размер машинного слова.

, то с std:map — нет, так как шаблон std::map требует минимум два параметра — тип ключа и тип значения. Таким образом, на данном этапе мы больше не можем использовать шаблон функции — нам нужно более универсальное решение. Прежде чем разбираться, как добавить поддержку контейнеров, давайте вспомним, что у нас еще есть пользовательские классы. Очевидно, что даже используя текущее решение, было бы не очень разумно перегружать функцию write для каждого класса, требующего сериализации. В лучшем случае мы бы хотели иметь одну специализацию шаблона write, который бы работал с пользовательскими типами данных. Но для этого необходимо, чтобы классы имели возможность самостоятельно управлять сериализацией, соответственно, у них должен появиться интерфейс, который бы позволил пользователю сериализовать и десериализовать объекты данного класса. Как выяснится чуть позже, данный интерфейс и послужит “общим знаменателем” для шаблона write при работе с пользовательскими классами. Давайте определим его.

Любой класс, наследуемый от ISerializable, обязуется:

  1. Переопределить serialize — запись состояния (членов-данных) в поток вывода.
  2. Переопределить deserialize — чтение состояния (инициализация членов-данных) из потока ввода.
  3. Переопределить serialized_size — вычисление размера сериализованных данных для текущего состояния объекта.

Нам каждый раз приходилось бы приводить тип наследника к ISerializable, чтобы воспользоваться данной специализацией. Напомню, что в самом начале мы поставили своей целью упростить написание кода, связанного с сериализацией, а не наоборот, усложнить его. Итак, если поддерживаемые нашей библиотекой типы не ограничиваются фундаментальными типами, то нам пора искать другое решение.

3. stream_writer

Использование шаблонов функций для реализации универсального интерфейса записи данных в поток оказалось не вполне пригодным решением. Следующий вариант, который нам стоит проверить — шаблон класса. Мы будем действовать по той же методике, которую использовали с шаблоном функции — первичный шаблон будет использоваться по умолчанию, а для поддержки необходимых типов будут добавлены явные специализации.

Кроме того, нам следует учесть все вышесказанное об ISerializable — очевидно, мы не сможем решить проблему с множеством классов-наследников, не прибегнув к type_traits: начиная с С++11 в стандартной библиотеке появился шаблон std::enable_if, позволяющий игнорировать шаблонные классы при определенных условиях во время компиляции — и именно этой возможностью мы собираемся воспользоваться.

Шаблон класса stream_writer:

Определение метода write:

Специализация для ISerializable будет выглядеть следующим образом:

, где only_if_serializable представляет собой вспомогательный тип:

Таким образом, если тип T является классом производным от ISerializable, то данная специализация будет рассмотрена в качестве кандидата на инстанцирование, соответственно, если тип T не находится в одной иерархии классов с ISerializable — она будет исключена из возможных кандидатов.

Довольно справедливо было бы задать здесь следующий вопрос: как это будет работать? Ведь первичный шаблон будет иметь те же значения типовых параметров, что и его специализация — <T, void>. Почему предпочтение будет отдано именно специализации, и будет ли? Ответ: будет, так как такое поведение предписано стандартом (источник):

Специализация для std::string теперь будет выглядеть следующим образом:

, где only_if_string объявлен как:

Настало время вернуться к контейнерам. В данном случае мы можем использовать тип контейнера параметризированный каким-либо типом U, или <U, V>, как в случае с std::map, непосредственно в качестве значения параметра T шаблона класса stream_writer. Таким образом, в интерфейсе у нас ничего не меняется — к этому мы и стремились. Однако, встает вопрос, каким должен быть второй параметр шаблона класса stream_writer, чтобы все работало корректно? Об этом — в следующей главе.

4. Концепты

Для начала я дам краткое описание использованных концептов, а уже потом покажу обновленные примеры.

Честно говоря, данный концепт был определен для махинации, которую мы увидим уже на следующей строке:

Container содержит требования, которые мы “предъявляем” типу, чтобы действительно убедиться, что он представляет собой один из контейнерных типов. Это именно тот набор требований, который понадобится нам при реализации stream_writer, стандарт предъявляет гораздо больше требований, разумеется.

Концепт для последовательных контейнеров: std::vector, std::list и т.д.

Концепт для ассоциативных контейнеров: std::map, std::set, std::unordered_map и т.д.

Теперь, чтобы определить специализацию для последовательных контейнеров все, что нам остается сделать, наложить ограничения на тип T:

  • std::vector
  • std::deque
  • std::list
  • std::forward_list
  • std::map
  • std::unordered_map
  • std::set
  • std::unordered_set

Для std::set все остается без изменений:

Для std::map — убираем const:

Определение read для ассоциативных контейнеров:

5. Вспомогательные функции

Определение метода serialize(std::ostream&) для данного класса должно было выглядеть следующим образом:

Однако, согласитесь, это неудобно, каждый раз указывать тип объекта, который записывается в поток вывода. Напишем вспомогательную функцию, которая бы автоматически выводила тип T:

Теперь определение выглядит следующим образом:

Для завершающей главы потребуется еще несколько вспомогательных функций:

Функция write_all позволяет перечислить сразу все объекты, подлежащие сериализации, в то время как write_recursive обеспечивает правильный порядок записи в поток вывода. Если бы для fold-expressions был определен порядок вычислений (при условии, что мы используем бинарный оператор +), можно было бы использовать их. В частности, в функции size_of_all (ранее не была упомянута, используется для вычисления размера сериализованных данных), используются именно fold-expressions ввиду отсутствия операций ввода-вывода.

6. Генерация кода

Для генерации кода используется libclang — C API для clang. Высокоуровнево данную задачу можно описать так: нам необходимо рекурсивно обойти директорию с исходным кодом, проверить все заголовочные файлы на наличие классов, помеченных специальным атрибутом, и если таковой присутствует — проверить члены-данные на наличие того же атрибута и скомпилировать строку из имен членов-данных, перечисленных через запятую. Все, что нам остается сделать, написать шаблоны определений для функций класса ISerializable (в которые нам остается поместить только перечисление необходимых членов данных).

Пример класса, для которого будет сгенерирован код:

Атрибуты записаны в GNU стиле, так как libclang отказывается распознавать формат атрибутов из C++20, и не аннотированные атрибуты он тоже не поддерживает. Обход директорий с исходным кодом:

Определение функции processTranslationUnit:

В данной функции интерес для нас представляет только ClassExtractor — все остальное необходимо для формирования AST. Определение функции extract выглядит следующим образом:

Здесь мы уже видим непосредственно С API функции для clang. Мы намеренно оставили только тот код, который необходим для понимания того, как используется libclang. Все, что остается “за кулисами”, не содержит важной информации — это всего лишь регистрация имен классов, членов-данных и т.п. Более подробный код может быть найден в репозитории.

Ну и, наконец, в функции processClass проверяется наличие атрибутов сериализации у каждого найденного класса, и, если таковой имеется — генерируется файл с определением необходимых функций. В репозитории представлены конкретные примеры: где взять имя/имена namespace’ов (данная информация хранится непосредственно в классе Class) и путь к заголовочному файлу.

Для вышеупомянутой задачи используется библиотека Argentum, которую я, к сожалению, Вам использовать не рекомендую — я начал ее разработку для иных целей, но ввиду того, что для данной задачи мне как раз понадобился функционал, который там был реализован, а я — ленив, я не стал переписывать код, а просто выложил ее на Bintray и подключаю в CMake файле через менеджер пакетов Conan. Все, что предоставляет эта библиотека — простые обертки над clang C API для классов и членов-данных.

И еще одно маленькое замечание — я не предоставляю готовую библиотеку, я лишь рассказываю, как ее написать.

UPD0: вместо libclang можно использовать cppast. Спасибо masterspline за предоставленную ссылку.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *