Бинарные файлы. BinaryWriter и BinaryReader


Для работы с бинарными файлами предназначена пара классов BinaryWriter и BinaryReader. Эти классы позволяют читать и записывать данные в двоичном формате.

BinaryWriter
Для создания объекта BinaryWriter можно применять ряд конструкторов. Возьмем наиболее простую:

BinaryWriter(Stream stream)
в его конструктор передается объект Stream (обычно это объект FileStream).

Основные методы класса BinaryWriter

Close(): закрывает поток и освобождает ресурсы

Flush(): очищает буфер, дописывая из него оставшиеся данные в файл

Seek(): устанавливает позицию в потоке

Write(): записывает данные в поток. В качестве параметра этот метод может принимать значения примитивных данных:

Write(bool)

Write(byte)

Write(char)

Write(decimal)

Write(double)

Write(Half)

Write(short)

Write(int)

Write(long)

Write(sbyte)

Write(float)

Write(string)

Write(ushort)

Write(uint)

Write(ulong)

Либо можно передать массивы типов byte и char

Write(byte[])

Write(char[])

Write(ReadOnlySpan<byte>)

Write(ReadOnlySpan<char>)

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

Write(byte[], int, int)

Write(char[], int, int)

Рассмотрим простейшую запись бинарного файла:

string path = "person.dat";

// создаем объект BinaryWriter
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.OpenOrCreate)))
{
// записываем в файл строку
writer.Write("Tom");
// записываем в файл число int
writer.Write(37);
Console.WriteLine("File has been written");
}
Здесь в файл person.dat записываются два значения: строка "Tom" и число 37. Для создание объекта применяется вызов new BinaryWriter(File.Open(path, FileMode.OpenOrCreate))

Подобным образом можно сохранять более сложные данные. Например, сохраним в файл массив объектов:

string path = "people.dat";

// массив для записи
Person[] people =
{
new Person("Tom", 37),
new Person("Bob", 41)
};

using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.OpenOrCreate)))
{
// записываем в файл значение каждого свойства объекта
foreach (Person person in people)
{
writer.Write(person.Name);
writer.Write(person.Age);
}
Console.WriteLine("File has been written");
}

class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
В данном случае последовательно сохраняем в файл people.dat данные объектов Person из массива people.

BinaryReader
Для создания объекта BinaryReader можно применять ряд конструкторов. Возьмем наиболее простую версию:

Reader(Stream stream)
в его конструктор также передается объект Stream (также обычно это объект FileStream).

Основные методы класса BinaryReader

Close(): закрывает поток и освобождает ресурсы

ReadBoolean(): считывает значение bool и перемещает указатель на один байт

ReadByte(): считывает один байт и перемещает указатель на один байт

ReadChar(): считывает значение char, то есть один символ, и перемещает указатель на столько байтов, сколько занимает символ в текущей кодировке

ReadDecimal(): считывает значение decimal и перемещает указатель на 16 байт

ReadDouble(): считывает значение double и перемещает указатель на 8 байт

ReadInt16(): считывает значение short и перемещает указатель на 2 байта

ReadInt32(): считывает значение int и перемещает указатель на 4 байта

ReadInt64(): считывает значение long и перемещает указатель на 8 байт

ReadSingle(): считывает значение float и перемещает указатель на 4 байта

ReadString(): считывает значение string. Каждая строка предваряется значением длины строки, которое представляет 7-битное целое число

С чтением бинарных данных все просто: соответствующий метод считывает данные определенного типа и перемещает указатель на размер этого типа в байтах, например, значение типа int занимает 4 байта, поэтому BinaryReader считает 4 байта и переместит указатель на эти 4 байта.

Например, выше в примере с BinaryWriter в файл person.dat записывалась строка и число. Считаем их с помощью BinaryReader:

using (BinaryReader reader = new BinaryReader(File.Open("person.dat", FileMode.Open)))
{
// считываем из файла строку
string name = reader.ReadString();
// считываем из файла число
int age = reader.ReadInt32();
Console.WriteLine($"Name: {name} Age: {age}");
}
Конструктор класса BinaryReader также в качестве параметра принимает объект потока, только в данном случае устанавливаем в качестве режима FileMode.Open: new BinaryReader(File.Open("person.dat", FileMode.Open)).

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

Или подобным образом считаем данные из файла people.dat, который был записан в примере выше и который содержит данные объектов Person:

// список для считываемых данных
List<Person> people = new List<Person>();

// создаем объект BinaryWriter
using (BinaryReader reader = new BinaryReader(File.Open("people.dat", FileMode.Open)))
{
// пока не достигнут конец файла
// считываем каждое значение из файла
while (reader.PeekChar() > -1)
{
string name = reader.ReadString();
int age = reader.ReadInt32();
// по считанным данным создаем объект Person и добавляем в список
people.Add(new Person(name, age));
}
}
// выводим содержимое списка people на консоль
foreach(Person person in people)
{
Console.WriteLine($"Name: {person.Name} Age: {person.Age}");
}

class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
Здесь в цикле while считываем данные. Чтобы узнать окончание потока, вызываем метод PeekChar(). Этот метод считывает следующий символ и возвращает его числовое представление. Если символ отсутствует, то метод возвращает -1, что будет означать, что мы достигли конца файла.

В цикле последовательно считываем значения для свойств объектов Person в том же порядке, в каком они записывались.