JavaRush /Курсы /C# SELF /Глубже в бинарный формат и его проблемы

Глубже в бинарный формат и его проблемы

C# SELF
43 уровень , 3 лекция
Открыта

1. Введение

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

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

Схема: сравнение форматов

Формат Читаем человеком Объем файла Скорость (запись/чтение) Совместимость
XML/JSON Да Большой Медленнее Хорошая
Бинарный Нет Малый Очень быстро Ограничена

Как работает бинарная сериализация в .NET?

В .NET-экосистеме исторически был главный инструмент для бинарной сериализации — класс BinaryFormatter. Но с развитием платформы он был признан небезопасным и исключён из .NET 9. Сейчас стандартом стали другие способы: BinaryWriter/BinaryReader, а для сложных объектов — сторонние библиотеки (например, protobuf-net).

Коротко о старом (исторический экскурс)

BinaryFormatter умел брать любой помеченный атрибутом [Serializable] класс и превращать его в байты, а при десериализации восстанавливать структуру объекта. Звучит магически, но в этом кроется масса проблем (об этом ниже).

Современные средства

Для примитивных типов и простых структур удобно использовать классы BinaryWriter и BinaryReader. Для сложных объектов — сторонние библиотеки (например, protobuf-net, MessagePack-CSharp и др.).

2. Сериализация примитивов с помощью BinaryWriter

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

public class UserProfile
{
    public string Name { get; set; }
    public int Score { get; set; }
    public DateTime LoginTime { get; set; }
}

public static Task SaveUserProfile(UserProfile profile, string filePath)
{
    // Открываем файл для записи
    using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
    using var writer = new BinaryWriter(stream);

    // Записываем данные по частям. Сначала строку, затем число, затем дату
    writer.Write(profile.Name ?? string.Empty); // строка
    writer.Write(profile.Score);                // целое число
    writer.Write(profile.LoginTime.ToBinary()); // дата приводится к "long"
}

Теперь пример чтения:

public static Task<UserProfile> LoadUserProfile(string filePath)
{
    using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    using var reader = new BinaryReader(stream);

    string name = reader.ReadString();
    int score = reader.ReadInt32();
    long dateData = reader.ReadInt64();
    DateTime loginTime = DateTime.FromBinary(dateData);

    return new UserProfile { Name = name, Score = score, LoginTime = loginTime };
}

Особенности примитивной бинарной сериализации

С помощью BinaryWriter мы сериализуем каждое свойство по отдельности. Это надёжный и предсказуемый способ: если структура данных меняется, мы видим это в коде.

3. Проблемы классической бинарной сериализации

Теперь посмотрим на обратную сторону медали. Почему, собственно, Microsoft так агрессивно осуждала BinaryFormatter и даже запретила его использование?

Хрупкость формата (Schema Evolution Hell)

Бинарные данные жёстко завязаны на структуру класса. Изменили класс (переименовали поле, добавили новое, удалили старое) — старые бинарные файлы стали нечитаемыми. Изменили порядок свойств — тоже проблема.

Иллюстрация:

// Вчера
public class Profile
{
    public string Name;
    public int Score;
}

// Сегодня
public class Profile
{
    public string Name;
    public double Rating; // Добавилось новое поле
    public int Score;
}

Чтение старого файла вызовет либо ошибку, либо прочитает поля с "смешанными" данными. В отличие от JSON или XML, где отсутствующие элементы можно пропустить, бинарный формат не подстраивается под изменения — он как плотная бетонная дорожка: слегка отошёл вбок — и сразу "падение велосипеда".

Уязвимости десериализации

Самая большая проблема BinaryFormatter — потенциальная уязвимость. Если ваш софт десериализует бинарные данные, полученные из ненадёжного источника (например, от пользователя из интернета), злоумышленник может подложить вредоносный "объект". В прошлом это приводило даже к удалённому выполнению произвольного кода на машине жертвы.

Кроссплатформенность и совместимость

Бинарный сериализатор жёстко завязан на внутреннее представление данных в .NET и текущей версии среды выполнения, компилятора и архитектуры (например, x64/ARM). Если вы сериализуете на Windows и пробуете десериализовать на Linux — сюрпризы обеспечены! Даже между версиями .NET могут возникнуть несовместимости.

Неудобство диагностики

При возникновении проблем с текстовыми форматами можно открыть файл, посмотреть на содержимое и догадаться, что пошло не так. Бинарный файл — это загадка семи печатей. Всё, что видят ваши глаза, — бессмысленный поток байтов. "Анализировать" такой файл — удовольствие на любителя.

4. Бинарная сериализация сложных объектов

Ссылки

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

Циклические ссылки

Сериализация объектов с зацикленными ссылками (например, у "мамы" свойство Child, а у "ребёнка" — свойство Parent, указывающее на родителя) либо вызывает ошибку, либо приводит к бесконечному циклу.

Пример:

public class Node
{
    public Node? Next { get; set; }
    public Node? Prev { get; set; }
}

Попытка сериализовать этот объект "по наивному" приведёт к зацикливанию.

5. Бинарная сериализация и переносимость

Любой бинарный формат (особенно самописный) — это формат "только для своих". Если вы собираетесь обмениваться данными с другими программами или сохранять их "на века" — выбирайте открытые стандарты: JSON, XML или ProtoBuf.

Когда бинарная сериализация оправдана?

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

Альтернативы: protobuf, MessagePack и другие

  • protobuf-net: порт Google Protocol Buffers для .NET, подходит для кроссплатформенных обменов и совместимости.
  • MessagePack-CSharp: быстрая реализация MessagePack для .NET.

В отличие от "сырого" BinaryWriter, эти библиотеки реализуют схемы, поддерживают эволюцию формата, кроссплатформенность и безопасность. Используйте их, если планируете хоть какую-то совместимость с другими системами.

6. "Ручная" бинарная сериализация

Если вам всё-таки нужно записывать бинарные данные (например, для приложений, где важна производительность), используйте BinaryWriter/BinaryReader — и всегда явно кодируйте порядок и тип данных.

Советы:

  • Всегда записывайте данные в одном и том же порядке, в каком планируете их читать.
  • При изменении структуры файла поддерживайте номер версии или пишите "магический заголовок" (Magic Header).
  • Добавляйте длину строк/массивов перед записью самих данных.
  • Документируйте структуру файла: иначе через год вы сами не поймёте свой формат.

Пример: версионирование

// Сохраняем номер версии формата на первом месте
writer.Write((byte)1); // Версия 1

writer.Write(profile.Name ?? "");
writer.Write(profile.Score);
writer.Write(profile.LoginTime.ToBinary());

/*
Позволяет при изменении формата впоследствии добавить условия чтения
*/

7. Типичные ошибки при работе с бинарной сериализацией

Закодировали поля в одном порядке, при чтении поменяли местами. В результате значения "съезжают": строка читается как int, int — как дата и т.д.

Записали 10 объектов, а читаете 11. Нарушается поток: возникнет исключение о достижении конца файла.

Изменили структуру класса, а старые бинарные файлы читать невозможно — теряется вся история данных.

Забыли обработать исключения при чтении важного файла — приложение падает при первом сбое на диске (например, EndOfStreamException).

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

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

2
Задача
C# SELF, 43 уровень, 3 лекция
Недоступна
Запись пользовательских данных в бинарный файл
Запись пользовательских данных в бинарный файл
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ