JavaRush /Курсы /C# SELF /Работа с байтовыми потоками

Работа с байтовыми потоками

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

1. Введение

Мы уже знакомы с потоками (Stream), которые представляют собой абстракцию для последовательного чтения или записи данных. Мы работали с FileStream для доступа к файлам на байтовом уровне, а также использовали StreamReader и StreamWriter для удобной работы с текстовыми данными, которые при этом под капотом используют FileStream и заботятся о кодировках.

Но что, если нам нужно хранить в файле не просто текст, а строго типизированные данные: целые числа (int), числа с плавающей точкой (double, float), булевы значения (bool), даты (DateTime) или даже пользовательские структуры? Конечно, можно преобразовать всё это в строки и записывать с помощью StreamWriter, а затем парсить обратно при чтении. Однако такой подход имеет существенные недостатки:

  • Неэффективность хранения: Число 12345, записанное как текст, занимает 5 байт (символов). В бинарном виде int занимает всего 4 байта. Для больших объемов данных разница становится критичной.
  • Производительность: Постоянные преобразования чисел в строки и обратно — это накладные расходы на процессорное время.
  • Точность данных: Преобразование дробных чисел в текст и обратно может привести к потере точности из-за округления.
  • Сложность парсинга: Ручная разборка текстовых строк для извлечения различных типов данных (например, "123,45 TRUE 2024-06-21") значительно усложняет код и делает его хрупким.

Для решения этих задач существуют специализированные классы BinaryReader и BinaryWriter. Эти классы являются специализированными адаптерами, которые работают поверх любого базового Stream (чаще всего FileStream) и предоставляют удобные методы для чтения и записи примитивных типов данных C# в их бинарном формате. Они берут на себя всю работу по преобразованию байтов в конкретные типы и обратно, существенно упрощая процесс работы со структурированными бинарными файлами.

Ключевая идея: BinaryReader и BinaryWriter — это не самостоятельные потоки. Они усиливают функциональность существующего Stream, добавляя методы для работы с C#-типами вместо сырых байтов.

2. Запись данных с помощью BinaryWriter

BinaryWriter предоставляет набор методов Write(), перегруженных для каждого примитивного типа данных C#. При вызове такого метода, BinaryWriter преобразует значение в его бинарное представление (последовательность байтов) и записывает эти байты в базовый Stream.

Пример: Сохраняем настройки игры

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


string filePath = "settings.bin";

// 1. Создаем FileStream для записи
using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
// 2. Создаем BinaryWriter поверх FileStream, указываем кодировку для строк (если будут)
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);
// 3. Записываем различные типы данных
writer.Write(0.75f);       // float (4 байта)
writer.Write(15);          // int (4 байта)
writer.Write(true);        // bool (1 байт)
writer.Write("Easy");      // string (префикс длины + байты)
                
Console.WriteLine($"Настройки сохранены в '{filePath}'.");

Разбор примера:

  • Мы создаем FileStream с FileMode.Create, что создает новый файл или перезаписывает существующий.
  • Затем создаем BinaryWriter, передавая ему fs. Важно, что BinaryWriter по умолчанию закрывает базовый поток (fs) при вызове его метода Dispose() (который автоматически вызывается using блоком).
  • Методы writer.Write() интуитивно понятны: Write(float), Write(int), Write(bool), Write(string). Они сами знают, сколько байт нужно записать для каждого типа и как их представить.
  • Для строк BinaryWriter автоматически добавляет префикс длины перед самими байтами строки. Это позволяет BinaryReader точно знать, сколько байтов нужно прочитать для восстановления строки.
  • Попытка открыть settings.bin в текстовом редакторе покажет вам "мусор", так как это — бинарный файл. Для просмотра содержимого нужен HEX-редактор.

3. Чтение данных с помощью BinaryReader

BinaryReader предоставляет методы ReadXxx() (например, ReadInt32(), ReadBoolean(), ReadString()), которые считывают соответствующее количество байтов из базового Stream и преобразуют их в нужный тип данных C#.

Пример: Загружаем настройки игры

Теперь прочитаем настройки из файла settings.bin, который мы создали ранее.


string filePath = "settings.bin";

// 1. Создаем FileStream для чтения
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// 2. Создаем BinaryReader поверх FileStream, используем ту же кодировку
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);

// 3. Читаем данные в ТОМ ЖЕ ПОРЯДКЕ, в котором они были записаны
float volume = reader.ReadSingle();     // float
int level = reader.ReadInt32();         // int
bool isMusicOn = reader.ReadBoolean();  // bool
string difficulty = reader.ReadString(); // string
                
Console.WriteLine($"Настройки загружены из '{filePath}':");
Console.WriteLine($"- Громкость: {volume:P0}"); // Форматируем как процент (используем форматирование строк)
Console.WriteLine($"- Уровень игрока: {level}");
Console.WriteLine($"- Музыка включена: {isMusicOn}");
Console.WriteLine($"- Сложность: {difficulty}");

Разбор примера:

  • Открываем FileStream в режиме FileMode.Open для чтения.
  • Создаем BinaryReader поверх fs, указывая ту же кодировку, что и при записи.
  • Критически важно: Порядок вызовов методов reader.ReadXxx() должен СТРОГО соответствовать порядку, в котором данные были записаны с помощью BinaryWriter. Если вы попытаетесь прочитать string там, где был записан int, это приведет к ошибке EndOfStreamException (если string длиннее) или чтению некорректных данных.
  • Методы ReadXxx() автоматически считывают нужное количество байтов и преобразуют их в запрошенный тип. ReadString() использует тот самый префикс длины, записанный BinaryWriter, чтобы определить, сколько байтов нужно прочитать для полной строки.

4. Важные нюансы и лучшие практики

Строгий порядок:

Это главное правило. BinaryReader и BinaryWriter не хранят метаданных о типах; они просто знают, сколько байтов занимает каждый примитивный тип. Вы должны обеспечивать соответствие порядка.

Управление ресурсами (using):

Как и большинство классов .NET, которые работают с системными ресурсами (например, файлами или сетевыми соединениями), и BinaryReader, и BinaryWriter реализуют интерфейс IDisposable. Поэтому всегда оборачивайте их в using-блок — так вы гарантируете автоматический вызов Dispose(), даже если произойдёт ошибка. Это защитит вас от утечек и правильно закроет файл.

Кстати, по умолчанию BinaryWriter и BinaryReader также вызовут Dispose() для переданного им базового потока (например, FileStream), так что он тоже будет закрыт автоматически.


using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
// ... работа

Кодировка для строк:

Для корректной работы со строками, переданными в BinaryWriter.Write(string) и считанными BinaryReader.ReadString(), обязательно указывайте одну и ту же кодировку в их конструкторах (например, Encoding.UTF8). Без этого могут возникнуть проблемы с символами, не входящими в ASCII.

Обработка исключений:

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

BaseStream и позиция:

Вы можете получить доступ к базовому потоку через свойство BaseStream (например, reader.BaseStream или writer.BaseStream). Это полезно, если вам нужно узнать текущую позицию (BaseStream.Position) или переместиться по файлу (BaseStream.Seek()).


// Пример использования BaseStream.Position
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(123);
Console.WriteLine($"Текущая позиция в потоке: {writer.BaseStream.Position}"); // Выведет 4 (размер int)

writer.Write("Hello");
Console.WriteLine($"Текущая позиция в потоке: {writer.BaseStream.Position}"); // Выведет 4 + (1+5) = 10

⚠️ Метод Write(string) сначала записывает длину строки в виде 7-битного целого, а затем сами байты строки. Поэтому итоговый размер не всегда равен 1 + длина строки.

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