C# — Сериализация или бинарный запись в файл C#


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

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

Во многих случаях сохранение данных приложения с использованием служб сериализации выливается в код меньшего объема, чем применение классов для чтения/записи из пространства имен System.IO.

Например, предположим, что создано настольное приложение с графическим интерфейсом, в котором необходимо предоставить конечным пользователям возможность сохранения их предпочтений (цвета окон, размер шрифта и т.п.). Для этого можно определить класс по имени UserPrefs и инкапсулировать в нем примерно два десятка полей данных. В случае применения типа System.IO.BinaryWriter придется вручную сохранять каждое поле объекта UserPrefs.

Аналогично, когда вам понадобится загрузить данные из файла обратно в память, придется использовать SystemIO.BinaryReader и, опять-таки, вручную читать каждое значение, чтобы реконструировать новый объект UserPrefs.

Сэкономить значительное время можно, снабдив класс UserPrefs атрибутом [Serializable]:

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

Хотя сохранять объекты с помощью механизма сериализации объектов .NET довольно просто, процесс, происходящий при этом «за кулисами», достаточно сложен. Например, когда объект сохраняется в потоке, все ассоциированные с ним данные (т.е. данные базового класса и содержащиеся в нем объекты) также автоматически сериализуются. Поэтому, при попытке сериализовать производный класс в игру вступают также все данные по цепочке наследования. И как будет показано, набор взаимосвязанных объектов, участвующих в этом, представляется графом объектов.

Службы сериализации .NET также позволяют сохранять граф объектов в различных форматах. В предыдущем примере кода применялся тип BinaryFormatter, поэтому состояние объекта UserPrefs сохраняется в компактном двоичном формате. Граф объектов можно также сохранить в формате SOAP или XML, используя другие типы форматеров. Эти форматы полезны, когда необходимо гарантировать возможность передачи хранимых объектов между разными операционными системами, языками и архитектурами.

В WCF предлагается слегка отличающийся механизм для сериализации объектов в и из операций службы WCF в нем используются атрибуты [DataContract] и [DataMember].

И, наконец, имейте в виду, что граф объектов может быть сохранен в любом типе, унаследованном от System.IO.Stream. В предыдущем примере объект UserPrefs был сохранен в локальном файле через тип FileStream. Однако если вместо этого понадобится сохранить объект в определенной области памяти, можно применить тип MemoryStream. Главное, чтобы последовательность данных корректно представляла состояние объектов в графе.

Роль графов объектов

Как упоминалось ранее, когда объект сериализуется, среда CLR учитывает все связанные объекты, чтобы гарантировать корректное сохранение данных. Этот набор связанных объектов называется графом объектов. Графы объектов представляют простой способ документирования набора отношений между объектами, и эти отношения не обязательно отображаются на классические отношения ООП (вроде отношений «является» и «имеет»), хотя достаточно хорошо моделируют эту парадигму.

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

В качестве простого примера предположим, что создан набор классов, моделирующих автомобили. Существует базовый класс по имени Car, который «имеет» класс Radio. Другой класс по имени JamesBondCar расширяет базовый тип Car. На рисунке показан возможный граф объектов, который моделирует эти отношения:

При чтении графов объектов для описания соединяющих стрелок можно использовать выражение «зависит от» или «ссылается на». Таким образом, на рисунке видно, что класс Car ссылается на класс Radio (учитывая отношение «имеет»), JamesBondCar ссылается на Car (учитывая отношение «имеет»), как и на Radio (поскольку наследует эту защищенную переменную-член).

Конечно, среда CLR не рисует картинок в памяти для представления графа взаимосвязанных объектов. Вместо этого отношение, документированное в предыдущей диаграмме, представлено математической формулой, которая выглядит примерно так:

Если вы проанализируете эту формулу, то опять увидите, что объект 3 (Car) имеет зависимость от объекта 2 (Radio). Объект 2 (Radio) — это «одинокий волк», которому не нужен никто. И, наконец, объект 1 (JamesBondCar) имеет зависимость как от объекта 3, так и от объекта 2. В любом случае, при сериализации или десериализации JamesBondCar граф объектов гарантирует, что типы Radio и Car также будут участвовать в процессе.

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

Запись текста в файл построчно на C#

Три самых простых способа как записать текст в файл построчно на C#. Во всех случаях используется using System.IO; . Напишу сразу примеры кода без всяких прелюдий.

Способ 1

Записать текстовый файл построчно:

Чтобы добавить текстовые данные в файл, просто укажите true после имени файла:

В этом случае, если файл не существует, то он будет создан новый, а если файл существует, он будет открыт для добавления данных. Соответственно, если второй параметр указать false, файл будет перезаписан.

Чтобы указать кодировку записываемого текстового файла (например, DOS-кодировку) можно сделать так:

Так же можно использовать обёртку using чтобы не нужно было закрывать файл после записи.

Способ 2

Запись с использованием FileStream.

Способ 3

Записать текстовый файл целиком из массива строк:

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

запись структуры в бинарный файл в C#

8 ответов

mailBox emailbox;
FileStream fs = new FileStream(textBox1.Text + «/account.dat», FileMode.Create);

emailbox.email = textBox1.Text;
emailbox.username = textBox2.Text;
emailbox.password = textBox3.Text;
emailbox.smtpServer = textBox4.Text;
emailbox.popServer = textBox5.Text;
emailbox.popBeforeSmtp = checkBox1.Checked ? 1 : 0;

IFormatter formatter = new BinaryFormatter();
try
<
formatter.Serialize(fs, emailbox);
>
catch (SerializationException ex)
<
MessageBox.Show(«Відбулася помилка: » + ex.Message, «Помилка»,
MessageBoxButtons.OK, MessageBoxIcon.Warning);
throw;
>
finally
<
fs.Close();
>

Спасибо за совет.
Пытаюсь записать, но сериализация не проходит, получаю ошибку, что данный объект сериализировать нельзя.

[Serializable]
public struct mailBox
<
public string email;
public string username;
public string password;
public string smtpServer;
public string popServer;
public int popBeforeSmtp;
>

mailBox emailbox;
FileStream fs = new FileStream(textBox1.Text + «/account.dat», FileMode.Create);

emailbox.email = textBox1.Text;
emailbox.username = textBox2.Text;
emailbox.password = textBox3.Text;
emailbox.smtpServer = textBox4.Text;
emailbox.popServer = textBox5.Text;
emailbox.popBeforeSmtp = checkBox1.Checked ? 1 : 0;

IFormatter formatter = new BinaryFormatter();
try
<
formatter.Serialize(fs, emailbox);
>
catch (SerializationException ex)
<
MessageBox.Show(«Відбулася помилка: » + ex.Message, «Помилка»,
MessageBoxButtons.OK, MessageBoxIcon.Warning);
throw;
>
finally
<
fs.Close();
>


а считываю на главной

[Serializable]
public struct mailBox
<
public string email;
public string username;
public string password;
public string smtpServer;
public string popServer;
public int popBeforeSmtp;
>

Как анализировать содержимое бинарного потока сериализации?

26 Tao [2010-06-16 12:51:00]

Я использую двоичную сериализацию (BinaryFormatter) как временный механизм для хранения информации о состоянии в файле для относительно сложной (игровой) структуры объекта; файлы выходят намного больше, чем я ожидаю, и моя структура данных включает в себя рекурсивные ссылки — поэтому мне интересно, действительно ли BinaryFormatter хранит несколько копий одних и тех же объектов или мой основной номер объектов и значений у меня должно быть «arithmentic» — это вне базы, или где еще происходит чрезмерный размер.

Поиск в переполнении стека Я смог найти спецификацию для формата удаленных файлов Microsoft: http://msdn.microsoft.com/en-us/library/cc236844(PROT.10).aspx

То, что я не могу найти, — это любой существующий просмотрщик, который позволяет вам «заглядывать» в содержимое выходного файла бинарного форматирования — получить количество объектов и общее количество байтов для разных типов объектов в файле и т.д.;

Я чувствую, что это должен быть мой «google-fu», который меня не сбивает (что у меня мало) — может ли кто-нибудь помочь? Это, должно быть, было сделано раньше, правильно?

UPDATE: я не мог найти его и не получил ответов, поэтому я поставил что-то относительно быстро (ссылка на загружаемый проект ниже); Я могу подтвердить, что BinaryFormatter не хранит несколько копий одного и того же объекта, но он печатает довольно много метаданных в потоке. Если вам нужно эффективное хранилище, создайте свои собственные методы сериализации.

c# .net serialization binary-serialization

4 ответа

48 Решение Markus Safar [2015-05-11 22:57:00]

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

Я основывал все свои исследования в спецификации .NET Remoting: Binary Format Data Structure.

Класс класса:

Чтобы иметь рабочий пример, я создал простой класс с именем A , который содержит 2 свойства, одну строку и одно целочисленное значение, они называются SomeString и SomeValue .

Класс A выглядит следующим образом:

Для сериализации я использовал BinaryFormatter , конечно:

Как можно видеть, я передал новый экземпляр класса A , содержащий abc и 123 в качестве значений.

Примеры данных результата:

Если мы посмотрим на сериализованный результат в шестнадцатеричном редакторе, мы получим что-то вроде этого:

Давайте интерпретировать данные результата примера:

В соответствии с вышеупомянутой спецификацией (вот прямая ссылка на PDF: [MS-NRBF].pdf) каждая запись в потоке идентифицируется RecordTypeEnumeration . Раздел 2.1.2.1 RecordTypeNumeration гласит:

Это перечисление идентифицирует тип записи. Каждая запись (кроме MemberPrimitiveUnTyped) начинается с перечисления типа записи. Размер перечисления — один BYTE.

SerializationHeaderRecord:

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

Как указано в 2.1.2.1 RecordTypeEnumeration , значение 0 идентифицирует SerializationHeaderRecord , указанное в 2.6.1 SerializationHeaderRecord :

Запись SerializationHeaderRecord ДОЛЖНА быть первой записью в двоичной сериализации. Эта запись имеет основную и второстепенную версию формата и идентификаторы верхнего объекта и заголовков.

  • RecordTypeEnum (1 байт)
  • RootId (4 байта)
  • HeaderId (4 байта)
  • MajorVersion (4 байта)
  • MinorVersion (4 байта)
Цукерберг рекомендует:  Учиться и кодить, кодить и учиться

С этими знаниями мы можем интерпретировать запись, содержащую 17 байт:

00 представляет RecordTypeEnumeration , который SerializationHeaderRecord в нашем случае.

01 00 00 00 представляет RootId

Если в потоке сериализации не присутствует ни BinaryMethodCall, ни BinaryMethodReturn, значение этого поля ДОЛЖНО содержать ObjectId записи Class, Array или BinaryObjectString, содержащейся в потоке сериализации.

Итак, в нашем случае это должно быть ObjectId со значением 1 (потому что данные сериализуются с использованием little-endian), которые мы будем надеяться увидеть снова; -)

FF FF FF FF представляет HeaderId

01 00 00 00 представляет MajorVersion

00 00 00 00 представляет MinorVersion

в BinaryLibrary:

Как указано, каждая запись должна начинаться с RecordTypeEnumeration . По завершении последней записи мы должны предположить, что начинается новая.

Давайте интерпретируем следующий байт:


Как мы видим, в нашем примере SerializationHeaderRecord следует запись BinaryLibrary :

Запись BinaryLibrary связывает идентификатор INT32 (как указано в разделе [2.2.22] MS-DTYP] с именем библиотеки. Это позволяет другим записям ссылаться на имя библиотеки с помощью идентификатора. Этот подход уменьшает размер проводов при наличии нескольких записей, которые ссылаются на одно и то же имя библиотеки.

  • RecordTypeEnum (1 байт)
  • LibraryId (4 байта)
  • LibraryName (переменное число байтов ( LengthPrefixedString ))

Как указано в 2.1.1.6 LengthPrefixedString .

LengthPrefixedString представляет собой строковое значение. Строка имеет префикс длины кодированной строки UTF-8 в байтах. Длина кодируется в поле переменной длины с минимумом 1 байт и не более 5 байтов. Чтобы свести к минимуму размер провода, длина кодируется как поле переменной длины.

В нашем простом примере длина всегда кодируется с помощью 1 byte . С помощью этих знаний мы можем продолжить интерпретацию байтов в потоке:

0C представляет RecordTypeEnumeration , который идентифицирует запись BinaryLibrary .

02 00 00 00 представляет LibraryId , который 2 в нашем случае.

Теперь LengthPrefixedString следует:

42 представляет информацию о длине LengthPrefixedString , которая содержит LibraryName .

В нашем случае информация о длине 42 (decimal 66) сообщает нам, что нам нужно прочитать следующие 66 байтов и интерпретировать их как LibraryName .

Как уже говорилось, строка UTF-8 закодирована, поэтому результат байтов выше будет примерно таким: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

ClassWithMembersAndTypes:

И снова запись завершена, поэтому мы интерпретируем RecordTypeEnumeration следующего:

05 идентифицирует запись ClassWithMembersAndTypes . В разделе 2.3.2.1 ClassWithMembersAndTypes указано:

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

  • RecordTypeEnum (1 байт)
  • ClassInfo (переменное число байтов)
  • MemberTypeInfo (переменное количество байтов)
  • LibraryId (4 байта)

ClassInfo:

Как указано в 2.3.1.1 ClassInfo , запись состоит из:

  • ObjectId (4 байта)
  • Имя (переменное число байтов (опять-таки LengthPrefixedString ))
  • MemberCount (4 байта)
  • MemberNames (который представляет собой последовательность LengthPrefixedString , где количество элементов ДОЛЖНО быть равно значению, указанному в поле MemberCount .)

Вернемся к исходным данным, шаг за шагом:

01 00 00 00 представляет ObjectId . Мы уже видели это, он был указан как RootId в SerializationHeaderRecord .

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 представляет Name класса, который представлен с помощью LengthPrefixedString . Как уже упоминалось, в нашем примере длина строки определяется с 1 байтом, поэтому первый байт 0F указывает, что 15 байтов должны быть прочитаны и декодированы с использованием UTF-8. Результат выглядит примерно так: StackOverFlow.A — поэтому я использовал StackOverFlow как имя пространства имен.

02 00 00 00 представляет MemberCount , он говорит нам, что последуют 2 члена, оба из которых представлены LengthPrefixedString .

Имя первого участника:

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 представляет первый MemberName , 1B — это снова длина строки длиной 27 байт, что приводит к чему-то вроде этого: k__BackingField .

Имя второго члена:

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 представляет второй MemberName , 1A указывает, что длина строки составляет 26 байтов. Это приводит к чему-то вроде этого: k__BackingField .

MemberTypeInfo:

После ClassInfo следует MemberTypeInfo .

Раздел 2.3.1.2 — MemberTypeInfo указывает, что структура содержит:

  • BinaryTypeEnums (переменная по длине)

Последовательность значений BinaryTypeEnumeration, которая представляет передаваемые типы-члены. Массив ДОЛЖЕН:

Имейте то же количество элементов, что и поле MemberNames структуры ClassInfo.

Будем упорядочиваться так, чтобы BinaryTypeEnumeration соответствовало имени члена в поле MemberNames структуры ClassInfo.

  • ДополнительноInfos (переменная по длине), в зависимости от BinaryTpeEnum дополнительная информация может быть или не быть.

Поэтому, учитывая это, мы почти там. Мы ожидаем 2 BinaryTypeEnumeration значений (потому что в MemberNames было 2 члена).

Снова вернемся к исходным данным полной записи MemberTypeInfo :

01 представляет BinaryTypeEnumeration первого члена, в соответствии с 2.1.2.2 BinaryTypeEnumeration можно ожидать a String , и оно представляется с помощью LengthPrefixedString .

00 представляет BinaryTypeEnumeration второго элемента, и, опять же, согласно спецификации, это Primitive . Как указано выше, за Primitive следует дополнительная информация, в данном случае a PrimitiveTypeEnumeration . Поэтому нам нужно прочитать следующий байт, который равен 08 , сопоставить его с таблицей, указанной в 2.1.2.3 PrimitiveTypeEnumeration , и удивляться тому, что мы можем ожидать Int32 , который представлен 4 байтами, как указано в некоторых другой документ об основных типах данных.

LibraryId:

После MemerTypeInfo следует LibraryId , он представлен 4 байтами:

02 00 00 00 представляет LibraryId , который равен 2.

Значения:

Как указано в 2.3 Class Records :

Значения членов класса ДОЛЖНЫ быть сериализованы как записи, которые следуют за этой записью, как указано в разделе 2.7. Порядок записей ДОЛЖЕН соответствовать порядку MemberNames, как указано в структуре ClassInfo (раздел 2.3.1.1).

Вот почему мы теперь можем ожидать значения членов.

Давайте посмотрим на последние несколько байтов:

06 идентифицирует BinaryObjectString . Он представляет ценность нашего свойства SomeString ( k__BackingField , если быть точным).

Согласно 2.5.7 BinaryObjectString он содержит:

  • RecordTypeEnum (1 байт)
  • ObjectId (4 байта)
  • Значение (переменная длина, представленная как LengthPrefixedString )

Поэтому, зная это, мы можем четко определить, что

03 00 00 00 представляет ObjectId .

03 61 62 63 представляет Value , где 03 — это длина самой строки, а 61 62 63 — это байты содержимого, которые переводятся на abc .

Надеюсь, вы помните, что был второй член, Int32 . Зная, что Int32 представляется с использованием 4 байтов, мы можем заключить, что

должен быть Value нашего второго члена. 7B шестнадцатеричный эквивалент 123 десятичный символ, который, по-видимому, соответствует нашему примеру.

Итак, вот полная запись ClassWithMembersAndTypes :

MessageEnd:

Наконец, последний байт 0B представляет запись MessageEnd .

7 Tao [2010-06-19 21:21:00]

Василий прав в том, что мне в конечном итоге нужно будет реализовать собственный процесс форматирования/сериализации, чтобы лучше обрабатывать управление версиями и выводить гораздо более компактный поток (до сжатия).

Я действительно хотел понять, что происходит в потоке, поэтому я написал (относительно) быстрый класс, который делает то, что я хотел:

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

Мне не очень удобно помещать его где-то видимым, как codeproject, поэтому я просто бросил проект в zip файл на моем сайте: http://www.architectshack.com/BinarySerializationAnalysis.ashx

В моем конкретном случае оказывается, что проблема двоякая:

  • BinaryFormatter ОЧЕНЬ многословный (это известно, я просто не понимал, в какой степени)
  • У меня были проблемы в моем классе, оказалось, что я хранили объекты, которые мне не нужны.

Надеюсь, это поможет кому-то в какой-то момент!

Обновление: Ян Райт связался со мной с проблемой с исходным кодом, где он разбился, когда исходный объект содержал «десятичные» значения. Теперь это исправлено, и я использовал случай, чтобы переместить код в GitHub и предоставить ему (разрешающую, BSD) лицензию.

Наше приложение использует массивные данные. Он может занимать до 1-2 ГБ оперативной памяти, например, в вашей игре. Мы столкнулись с проблемой «хранения нескольких копий одних и тех же объектов». Также двоичная сериализация хранит слишком много метаданных. Когда он был впервые реализован, сериализованный файл занял около 1-2 ГБ. В настоящее время мне удалось уменьшить стоимость — 50-100 МБ. Что мы сделали.

Короткий ответ — не используйте двоичную сериализацию .Net, создайте свой собственный механизм двоичной сериализации. У нас есть собственный класс BinaryFormatter и интерфейс ISerializable (с двумя методами Serialize, Deserialize).

Один и тот же объект не следует сериализовать более одного раза. Мы сохраняем его уникальный идентификатор и восстанавливаем объект из кеша.

Я могу поделиться некоторым кодом, если вы спросите.

EDIT: Кажется, вы правы. См. Следующий код — это доказывает, что я был неправ.


Похоже, BinaryFormatter использует object.Equals для поиска тех же объектов.

Вы когда-нибудь просматривали сгенерированные файлы? Если вы откроете «temp-file0.txt» и «temp-file1.txt» из примера кода, вы увидите, что у него много метаданных. Поэтому я рекомендовал вам создать собственный механизм сериализации.

Извините за то, что вы созрели.

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

Если это невозможно из-за размера игры или других зависимостей, вы всегда можете ухаживать за простым/маленьким приложением, которое включает в себя код десериализации и просмотр из режима отладки.

C# — Сериализация или бинарный запись в файл C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

// подключаем BinaryFormatter
using System.Runtime.Serialization.Formatters.Binary;

// подключаем FileStream
using System.IO;

namespace ConsoleApplication1
<
// класс для сериализации
[Serializable]
class Book
<
public string Name < get; set; >
public string Author < get; set; >
public int Price < get; set; >
>

// главный класс программы
class Program
<
static void Main( string [] args)
<
// объект
Book book1 = new Book();
book1.Name = «Властелин колец» ;
book1.Author = «Джорж Толкин» ;
book1.Price = 120;

// создаем BinaryFormatter
BinaryFormatter formatter = new BinaryFormatter();

// создаем поток байт (бинарный файл)
using (FileStream fs = new FileStream( «d://1.dat» , FileMode.OpenOrCreate))
<
// сериализация (сохранение объекта в поток байт)
formatter.Serialize(fs, book1);
>

// открываем поток байт (бинарный файл)
using (FileStream fs = new FileStream( «d://1.dat» , FileMode.OpenOrCreate))
<
// десериализация (создание объекта из потока байт)
Book book2 = (Book)formatter.Deserialize(fs);
>
>
>
>

BinaryFormatter класс для сериализации объекта в бинарном формате .

Синтаксис
public sealed > BinaryFormatter : IRemotingFormatter, IFormatter

.NET, C# и все-все-все

Мысли, переводы, аналитика на тематику .NET/C#

вторник, 28 февраля 2012 г.

Сериализация в .NET — это просто

Магические слова “сериализация” и “десериализация” имеют отношение к магии сохранения состояния объекта. Наверняка сейчас мало у кого может возникнуть вопрос “зачем нужна сериализация”, но дабы соответствовать духу статьи, сказать об этом необходимо.
Сериализация имеет прямое отношение к сохранению состояния объекта в файле или памяти, а десериализация — к восстановлению состояния объекта соответственно. Примеров, когда это может пригодиться, неимоверное множество. Начиная от сохранения пользовательских настроек, сохранения промежуточного состояния объекта и заканчивая передачей объекта в специальном формате веб-сервису.
Для осуществления магии (де)сериализации .NET предлагает нам 3 родных варианта (не считая самостоятельной реализации механизма сериализации):
Сериализация в двоичный формат (BinnaryFormatter)
Сериализация в формат SOAP (SoapFormatter)
Сериализация в формат xml (XmlSerializer)

У каждого из вариантов есть свои плюсы и минусы, о которых я постараюсь рассказать далее.

Создание класса для экспериментов Чтобы сделать объект сериализируемым нужно снабдить каждый связанный с ним класс или структуру аттрибутом [Serializable]. Если есть поля, которые по какой-то причине нужно исключить из сериализации, их необходимо пометить аттрибутом [NonSerialized].
Прежде чем приступить к рассмотрению имеющихся механизмов сериализации, давайте подготовим для экспериментов парочку простеньких классов: Сериализация в двоичный формат с помощью BinaryFormatter
BinaryFormatter сохраняет состояние объекта в двоичном формате. Сериализируются все поля, вне зависимости от их области видимости. Исключение составляют поля помеченные аттрибутом [NonSerialized]. Помимо сохранения данных полей, BinaryFormatter также сохраняет полное квалифицированное имя каждого типа, полное имя сборки, где он определен, сюда входит информация об имени, версии, маркере общедоступного ключа (public key) и культуре. Отсюда следует, что BinaryFormatter это идеальный выбор в ситуациях, когда необходимо сохранять полные копии объектов для дальнейшей их передачи по значению между доменами для использования в .NET приложениях. Здесь заключается основной минус BinaryFormatter — данные, сохраненные с его помощью, могут быть воссозданы только в инфраструктуре CLI. Причем каждый, кто будет восстанавливать данные, должен иметь сборку с сериализуемым типом.
Сериализация происходит с помощью двух ключевых методов Serialize() и Deserialize(). Первый сохраняет граф объектов в виде последовательности байт в указанный поток. Второй наоборот — преобразует сохраненную последовательность байт в граф объектов.
Теперь инициализируем описанный выше класс SuperHero и сохраним его состояние с помощью двоичной сериализации: Из примера видно, что поле age не сохранилось и все благодаря аттрибуту [NonSerialized]. В то же самое время состояние закрытого поля hasSecret сохранено успешно.
Важно помнить, что метод Deserialize() возвращает объект типа System.Object, поэтому необходимо явно привести объект к нужному типу.

Сериализация в SOAP формат с помощью SoapFormatter
SoapFormatter сохраняет состояние объекта в SOAP формате (Simple Object Access Protocol) и также как и BinaryFormatter сериализирует все поля, вне зависимости от их области видимости, кроме полей помеченных аттрибутом [NonSerialized]. В отличие от BinaryFormatter, платформа и операционная система не влияют на успешное восстановление данных, сериализированных с помощью SoapFormatter.
Как и в случае с BinaryFormatter (де)сериализация происходит с помощью ключевых методов Serialize() и Deserialize().
Сохраним состояние нашего объекта SuperHero в виде SOAP сообщения: В результате выполнения данного кода будет создан файл SuperHumanInfo.soap. Заглянув внутрь которого, можно увидеть XML-элементы, описывающие состояние нашего объекта superHuman с указателями ref, описывающими отношения между объектами в графе. Приватное поле hasSecret было также, как и в случае с BinaryFormatter, успешно сохранено.
Из двух механизмов сериализации: BinaryFormatter и SoapFormatter, рекомендуется использовать первый т.к. начиная с версии .NET 3.5, SoapFormatter считается устаревшим.

Сериализация в XML формат с помощью XmlSerializer
XmlSerializer не сохраняет приватные данные. Хотя это можно сделать, инкапсулировав такое поле в общедоступном свойстве: Также XmlSerializer не сохраняет точную информацию о типе (квалифицированное имя, имя сборки и т.д.), что делает его идеальным кандидатом когда необходимо сохранить объект для дальнейшего использования в другом языке программирования, а также на любой платформе, в любой операционной системе.
Сериализация с помощью XmlSerializer немного отличается от сериализации с помощью BinaryFormatter и SoapFormatter. XmlSerializer требует указания информации о типе, который нужно сериализовать. Также, если определялись конструкторы отличные от конструктора по умолчанию, то во избежание исключения InvalidOperationException, необходимо добавить конструктор по умолчанию.
А теперь сохраним, уже ставший нам родным объект типа SuperHero в формате XML: Состояние приватного поля hasSecret сохранить не удалось, в отличие от приватного поля name благодаря наличию общедоступного свойства Name.
Изначально XmlSerializer сохраняет данные объекта как XML элементы. С помощью ряда аттрибутов, которые есть в наличии у класса XmlSerializer, можно управлять генерацией итогового XML документа: сериализовать поле/свойство как XML аттрибут, сериализовать поле/свойство с определенным именем, сконструировать root элемент и т.д.

В заключение
Для сохранения состояния объекта можно воспользоваться одним из доступных в .NET механизмов сериализации:
BinaryFormatter
SoapFormatter
XmlSerializer

Выбор механизма зависит от поставленной задачи. Если нужна скорость, хорошее сжатие данных и вы работает в пределах инфраструктуры CLI, используйте BinaryFormatter.
Для передачи же сложных структур, когда необходимо представить данные в читабельном формате, и вы не хотите зависеть от платформы, языка программирования и т.д используйте XmlSerializer.
Также можно воспользоваться и SoapFormatter, но как я сказал ранее, начиная с версии .NET 3.5 этот класс считается устаревшим (obsolete).

Чтение и запись двоичных данных C#

Всем доброго времени суток. На связи Алексей Гулынин. В прошлой статье, посвященной потокам, мы рассмотрели чтение и запись символов или байтов, с помощью классов «StreamWriter» и «StreamReader». В данной статье я бы хотел рассмотреть чтение и запись двоичных данных в C#, ведь помимо строк, можно читать и записывать другие типы данных, например «int», «double», «short», «byte». Для чтения и записи двоичных значений типов данных , которые встроены в C#, служат классы потоков «BinaryReader» и «BinaryWriter». Когда вы используете данные потоки, помните, что данные считываются и записываются во внутреннем двоичном формате, а не в удобной для чтения текстовой форме.

Класс «BinaryWriter» служит переходником, в который заключается байтовый поток, управляющий выводом двоичных данных.

В данном классе существует метод «Write()», который является перегруженным и в качестве значений принимает встроенные в C# типы данных.

Объект данного класса вызывается следующим образом:

«buffer» — это поток, в который выводятся записываемые данные. В качестве параметра «buffer» можно указать объект класса «FileStream». Если параметр «buffer» является пустым, то выйдет исключение «ArgumentNullException». Если поток, который определяется параметром «buffer», не был открыт для записи данных, то выйдет исключение «ArgumentException». По завершении вывода поток «BinaryWriter» нужно закрывать.

Класс «BinaryReader» служит переходником, в который заключается байтовый поток, управляющий вводом двоичных данных. В данном классе находятся методы, предназначенные для чтения данных всех встроенных в C# типов.

Объект данного класса вызывается следующим образом:

Давайте на примере разберем работу данных классов:

Если просмотреть содержимое файла «test», то мы увидим, что он содержит данные в двоичной форме.

В данной статье вы узнали, как читать и записывать двоичные данные в C#.

На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

NFX — Ультраэффективная Бинарная Сериализация в CLR

В данной статье мы рассмотрим задачи переноса сложных объектов между процессами и машинами. В наших системах было много мест, где требовалось перемещать большое кол-во бизнес объектов различной структуры, например:


  • самозацикленные графы объектов (деревья с back-references)
  • массивы структур (value types)
  • классы/структуры с readonly полями
  • инстансы существующих .Net коллекций (Dictionary, List), которые внутренне используют custom-сериализацию
  • большое кол-во инстансов типов, специализированных для конкретной задачи

Речь пойдёт о трёх аспектах, которые очень важны в распределённых кластерных системах:

  • скорость сериализации/десериализации
  • объём объектов в сериализированном виде
  • возможность использовать существующие объекты без надобности “украшения” этих объектов и их полей вспомогательными атрибутами для сериализации

Кратко рассмотрим три вышеперечисленных аспекта.

Первое — скорость. Это очень важно для обеспечения общего быстродействия системы в распределённой среде, когда для выполнения задачи (например, запрос от одного пользователя) требуется исполнить пять-десять запросов на другие машины бэк-энда.

Второе — объём. При перекачке/репликации большого кол-ва данных бюджет канала связи между датацентрами не должен “раздуваться”.

Третье — удобство. Очень неудобно, когда только для сериализации/маршалинга требуется создание “лишних” объектов, ктр. переносят данные. Также неудобно заставлять программиста конкретного бизнес-типа писать низкоуровневый код по записи инстанса в массив байт. Может это и можно сделать, когда у вас 5-6 классов, но что делать, если в вашей системе 30 базовых generic классов (i.e. DeliveryStrategy), каждый из которых комбинируется с десятками других классов (это даёт сотни конкретных типов, i.e.: DeliveryStrategy, DeliveryStrategy, DeliveryStrategy etc.). Очень хотелось бы иметь прозрачную систему, которая может сериализировать практически все классы предметной области без надобности дополнительной разметки, кода и т.д. Конечно, есть вещи, которые не нужно сериализировать, например, какие-то unmanaged ресурсы или делегаты, но всё остальное обычно нужно, даже такие элементы как readonly поля структур и классов.

Данная статья освещает тему именно бинарной сериализации. Мы не будем говорить о JSON и прочих форматах, так как они не предназначены для эффективного решения вышеупомянутых задач.

Проблемы существующих сериализаторов

Сразу оговоримся, всё, что тут написано, относительно — смотря что с чем сравнивать. Если вы пишите/читаете сотни объектов в секунду, то проблем нет. Другое дело, когда нужно обрабатывать десятки или даже сотни тысяч объектов в секунду.

BinaryFormatter — ветеран .Net. Отличается простотой использования и подходит к требованиям лучше, чем DataContractSerializer. Хорошо поддерживает все встроенные типы коллекций и прочих BCL классов. Поддерживает версионность объектов. Не интероперабилен между платформами. Имеет очень большие недостатки связанные с производительностью. Он очень медленный и сериализация производит очень массивные потоки.

DataContractSerializer — движок WCF. Работает быстрее BinaryFormatter’а во многих случаях. Поддерживает интероперабильность и версионность. Однако этот сериализатор не предназначен для решения general-purpose проблем сериализации как таковой. Он требует специализированной декорации классов и полей атрибутами, также имеются проблемы с полиморфизмом и поддержкой сложных типов. Это очень объяснимо. Дело в том, что DataContractSerializer не предназначен по определению для работы с произвольными типами (отсюда и название).

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

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

Исходя из вышеперечисленных характеристик, если не учитывать производительность, самый подходящий для нас сериализатор — это BinaryFormatter. Он наиболее “прозрачен”. То, что он не поддерживает интероперабельность между платформами, для нас не важно, т.к. у нас одна платформа — Unistack. Но вот скорость его работы просто ужасная. Очень медленно и большой объём на выходе.

NFX.Serialization.Slim.SlimSerializer

SlimSerializer является гибридным сериализатором с динамической генерацией ser/deser кода в рантайме для каждого конкретного типа.

Мы не пытались сделать абсолютно универсальное решение, ибо тогда пришлось бы жертвовать чем-то. Мы не делали вещи, которые. для нас неважны, а именно:

  • кросс-платформенность
  • object version upgrade

Исходя из вышесказанного, SlimSerializer не подходит для таких задач, где:

  • данные хранятся в storage (например, на диске)
  • данные генерируются/принимаются процессами не на CLR-платформе, однако Windows.NET — to — Linux.MONO и Linux.MONO — to — Windows.NET работают великолепно

SlimSerializer предназначен для ситуаций, когда:

  • нужна большая скорость (сотни тысяч операций в секунду)
  • требуется экономить объём передаваемых данных
  • специализированная разметка для сериализации нереальна по разным причинам (например, очень много классов)

SlimSerializer поддерживает всевозможные edge-case’ы, например:

  • прямая сериализация примитивных структур и их Nullable эквивалентов (DateTime, Timespan, Amount, GDID, FID, GUID, MethodSpec, TypeSpec etc.)
  • прямая сериализация основных reference-типов (byte[], char[], string[])
  • поддержка классов и структур с read-only полями
  • поддержка custom-сериализации ISerializable, OnSerializing, OnSerialized… etc.
  • каскадно-вложенная сериализация (например, какой-то тип делает custom-сериализацию себя и должен вызвать SlimSerializer для какого-то поля)
  • позволяет сериализировать любые поддерживаемые типы (кроме делегатов) в корень
  • нормализует графы любой сложности и вложенности
  • детекция buffer-overflow в десериализации (это нужно, когда стрим корраптается и возможно непреднамеренное выделение большого куска памяти)

Разработка непростая и уже претерпела множество оптимизаций. Результаты, которых нам удалось достичь, не конечны, можно ещё ускорить, но это вызовет усложнение уже и без того нетривиального кода.

Как это работает?

SlimSeralizer использует стриммер, который берётся из injectable формата github.com/aumcode/nfx/blob/master/Source/NFX/IO/StreamerFormats.cs. Стриммер-форматы нужны для того, чтобы сериализировать определённые типы напрямую в поток. Например, мы по умолчанию поддерживаем такие типы как FID, GUID, GDID, MetaHandle etc. Дело в том, что определённые типы можно хитро паковать variable-bit энкодингом. Это даёт очень большой прирост в скорости и экономит место. Все integer-примитивы пишутся variable-bit энкодингом. Таким образом, в случаях, когда нужна супербыстрая поддержка специального типа, можно унаследовать StreamerFormat и добавить WriteX/ReadX методы. Система сама собирает и превращает их в лямбда-функторы, которые нужны для быстрой сериализации/десериализации.

Для каждого типа строится TypeDescriptor github.com/aumcode/nfx/blob/master/Source/NFX/Serialization/Slim/TypeSchema.cs., который динамически компилирует пару функторов для сериализации и десериализации.

SlimSerializer построен на идее TypeRegistry и это главная изюминка всего сериализатора github.com/aumcode/nfx/blob/master/Source/NFX/Serialization/Slim/TypeRegistry.cs. Типы пишутся как строка — полное имя типа, но если такой тип уже встречался ранее, то пишется type handle вида “$123”. Это обозначает типа, находящийся в регистратуре за номером 123.

Когда мы встречаем reference, то заменяем его на MetaHandle github.com/aumcode/nfx/blob/master/Source/NFX/IO/MetaHandle.cs, который эффективно инлайнает либо строку, если reference на string, либо integer, который является номером инстанса объекта в графе объектов, т.е. своеобразный псевдо-поинтер-хэндл. При десериализации всё реконструируется в обратном порядке.

Производительность

Все нижеприведённые тесты производились на Intel Core I7 3.2 GHz на одном потоке.
Производительность SlimSerializer масштабируется пропорционально кол-ву потоков. Мы применяем специализированные thread-static оптимизации, дабы не копировать буфера.

Возьмём следующий тип в качестве “подопытного”. Обратите внимание на всевозможные атрибуты, которые нужны для DataContractSerializer:

А теперь делаем много раз по 500 000 объектов:

  • Slim serialize: 464 252 ops/sec; size: 94 bytes
  • Slim deser: 331 564 ops/sec
  • BinFormatter serialize: 34 702 ops/sec: size: 1188 bytes
  • BinFormatter deser: 42 702 ops/sec
  • DataContract serialize: 108 932 ops/sec: size: 773 bytes
  • DataContract deser: 41 985 ops/sec

Скорость сериализации Slim к BinFormatter: в 13.37 раз быстрее.
Скорость десериализации Slim к BinFormatter: в 7.76 раз быстрее.
Объём Slim к BinFormatter: в 12.63 раз меньше.

Скорость сериализации Slim к DataContract: в 4.26 раз быстрее.
Скорость десериализации Slim к DataContract: в 7.89 раз быстрее.
Объём Slim к DataContract: в 8.22 раз меньше.

А теперь пробуем сложный object-граф из нескольких десятков взаимно ссылающихся объектов, включая массивы и листы (много раз по 50 000 объектов):

  • Slim serialize: 12 036 ops/sec; size: 4 466 bytes
  • Slim deser: 11 322 ops/sec
  • BinFormatter serialize: 2 055 ops/sec: size: 7 393 bytes
  • BinFormatter deser: 2 277 ops/sec
  • DataContract serialize: 3 943 ops/sec: size: 20 246 bytes
  • DataContract deser: 1 510 ops/sec


Скорость сериализации Slim к BinFormatter: в 5.85 раз быстрее.
Скорость десериализации Slim к BinFormatter: в 4.97 раз быстрее.
Объём Slim к BinFormatter: в 1.65 раз меньше.

Скорость сериализации Slim к DataContract: в 3.05 раз быстрее.
Скорость десериализации Slim к DataContract: в 7.49 раз быстрее.
Объём Slim к DataContract: в 4.53 раз меньше.

Обратите внимание на разницу при сериализации типизированного класса (первый случай “Perzon”) и второй (много объектов). Во втором случае есть сложный граф с циклическими взаимосвязями объектов и поэтому Slim начинает приближаться (замедляться) по скорости к Microsoft’у. Однако всё равно превосходит последний минимум в 4 раза по скорости и в полтора раза по объёму. Код на этот тест: github.com/aumcode/nfx/blob/master/Source/Testing/Manual/WinFormsTest/SerializerForm2.cs#L51-104

А вот здесь сравнение с Apache.Thrift: blog.aumcode.com/2015/03/apache-thrift-vs-nfxglue-benchmark.html.
Хоть эти цифры и не по чистой сериализации, а по всему NFX.Glue (который включает в себя мэссаджинг, TCP networking, security etc), скорость очень сильно зависит от SlimSerializer, на котором построены “родные” байндинги NFX.Glue.

Итоги

NFX SlimSerializer даёт исключительно высокий и предсказуемо устойчивый перформанс, экономя ресурсы процессора и памяти. Именно это открывает возможности для технологий высокой нагруженности на CLR платформе, позволяя обрабатывать сотни тысяч запросов в секунду на каждом узле distributed систем.

У SlimSerializer’а есть несколько ограничений, обусловленных невозможностью создать практическую систему “one size fits all”. Эти ограничения: отсутствие версионности структур данных, сериализации делегатов, интероперабильности с другими платформами кроме CLR. Однако стоит заметить, что в концепции Unistack (унифицированный стэк software для всех узлов системы) эти ограничения вообще незаметны кроме отсутствия версионности, т.е. SlimSerializer не предназначен для длительного хранения данных на диске, если структура данных может поменяться.

Ультра-эффективные native байндинги NFX.Glue позволяют обслуживать 100 тысяч + двусторонних вызовов (two-way calls) в секунду благодаря специализированным оптимизациям, применяемым в сериализаторе, при этом не требуя от программиста лишней работы по созданию extra data-transfer типов

SlimSerializer значительно обгоняет встроенные в .NET средства, позволяя эффективно обрабатывать сложные графы взаимосвязанных объектов (чего ни Protobuf, ни Thrift делать не умеют).

Чтение и запись данных в файл

Дата изменения: 10.10.2020

Чтение данных и запись данных в файл задача мене тривиальная, чем можно предположить. Функционал System.IO представляет несколько способов сделать это, в зависимости от задач и ограничений, которые стоят перед программистом.

Способы чтения и записи в файл:

  1. Статическими методами класса File.
  2. С помощью методов классов StreamReader и Streamwriter.
  3. С помощью методов классов BinaryReader и BinaryWriter.

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

Обьекты StreamReader и StreamWriter (BinaryReader и BinaryWriter) возвращаются методами класса FileInfo, как экземпляры, непосредственно работающие с файлом. Код становится более громоздким, но и более гибким, поскольку позволяет организовать сложную систему считывания/записи данных файл, внутри единого блока try/catch для обработки исключений.

Для использования любого из методов в этом разделе нужно включить в код программы библиотеку System.IO перед их вызовом.

Каждый из классов рассматривается нами подробно в отдельных разделах. Сейчас перейдем непосредственно к примерам.

Считывание данных из файла

Функционал класса File позволяет считывать из файла данные в виде строк. Основной метод, который используется при этом – ReadAllText.

При вызове через имя класса он:

  1. Открывает файл.
  2. Считывает все строки файла.
  3. Закрывает файл.

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

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

Запись в файл

Функционал записи текста в файл в классе File главным образом представлен методом WriteAllText. Метод имеет два параметра: строковое имя файла и строку или массив строк, которые нужно записать в файл. Метод сам откроет файл, запишет в него текст и закроет его. Весь код умещается в одну строку:

Те же самые иструкции легко выполнить методами класса StreamWriter.

Пример:

Запись в фалй с помощью средств класса StreamWriter можно выполнить асинхронноЮ если применить асинхронную версию метода записи. Например:

Запись текста в файл построчно на C#

Три самых простых способа как записать текст в файл построчно на C#. Во всех случаях используется using System.IO; . Напишу сразу примеры кода без всяких прелюдий.

Способ 1

Записать текстовый файл построчно:

Чтобы добавить текстовые данные в файл, просто укажите true после имени файла:

В этом случае, если файл не существует, то он будет создан новый, а если файл существует, он будет открыт для добавления данных. Соответственно, если второй параметр указать false, файл будет перезаписан.

Чтобы указать кодировку записываемого текстового файла (например, DOS-кодировку) можно сделать так:

Так же можно использовать обёртку using чтобы не нужно было закрывать файл после записи.

Способ 2

Запись с использованием FileStream.

Способ 3

Записать текстовый файл целиком из массива строк:

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

Цукерберг рекомендует:  Uml - UML VS Enterprise
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих