Содержание
Для работы с бинарными файлами в C# можно использовать как обычный FileStream
, так и более удобные для этих целей классы, а именно — Binary
и Binary
. Эти классы, содержат достаточно большое количество перегруженных версий методов, позволяющих записывать и считывать различные данные различных типов в бинарные файлы.
Запись бинарных файлов в C# (класс BinaryWriter)
Для создания экземпляра класса BinaryWriter
мы можем воспользоваться одним из следующих конструкторов:
BinaryWriter() |
Инициализирует новый экземпляр класса BinaryWriter , который осуществляет запись в поток. |
BinaryWriter(Stream) |
Инициализирует новый экземпляр класса BinaryWriter на основании указанного потока с использованием кодировки UTF-8. |
BinaryWriter(Stream, Encoding) |
Инициализирует новый экземпляр класса BinaryWriter на основе указанного потока и кодировки символов. |
BinaryWriter(Stream, Encoding, Boolean) |
Инициализирует новый экземпляр класса BinaryWriter на основании указанного потока и кодировки символов, а также при необходимости оставляет поток открытым. |
Для работы с бинарными данными у класса BinaryWriter
определен ряд методов, которые имеют перегруженные версии:
Метод | Описание |
---|---|
Flush() |
Очищает все буферы текущего модуля записи и вызывает немедленную запись всех буферизованных данных на базовое устройство. |
Seek(Int32, Seek |
Задает позицию в текущем потоке. См. работу аналогичного метода у FileStream . |
Write() |
Записывает значение различных типов в поток. Определены методы для записи чисел, символов, строк, значений boolean и т.д. |
Write7BitEncodedInt(Int32) |
Записывает 32-разрядное целое число в сжатом формате. |
Так как мы рассматриваем работу с бинарными файлами, то запишем в файл, например, объект такого класса:
public class Person { public string Name { get; set; } public int Age { get; set; } public double Weight { get; set; } }
Бинарные файлы, обычно, имеют строго определенную структуру, благодаря чему, мы можем получать доступ к определенной части данных и считывать или записывать их. В нашем классе «тонким» местом является имя. У разных людей длина имени различна — от одного символа до десятков символов и более. Соответственно, чтобы мы могли точно устанавливать позицию в файле для чтения данных, записывать этот файл мы должны строго по определенному формату. Для этого договоримся, что длина имени человека не должна превышать 100 символов, а если, друг, имя превысит 100 символов то оно будет обрезаться.
Таким образом мы получаем следующий формат бинарного файла:
- первые 100 байт файла отведены для имени (
Name
) - далее 4 байта для возраста (
Age
) - далее 8 байт для веса (
Weight
)
Итого, после записи одного объекта в файл его размер должен стать ровно 100+4+8 = 112 байт. Вот как может выглядеть запись объекта в файл в C#:
class Program { static byte[] CopyBytes(string value, int bufferSize) { byte[] result = new byte[bufferSize]; byte[] buffer = Encoding.Default.GetBytes(value); Array.Copy(buffer, result, buffer.Length < bufferSize ? buffer.Length : bufferSize); return result; } static void Main(string[] args) { Person person = new() { Name = "Иван Иванович Иванов", Age = 45, Weight = 70.5 }; string fileName = @"c:\CSharp Output\TextFile.dat"; using FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate); using BinaryWriter writer = new BinaryWriter(fs); byte[] nameBuffer = CopyBytes(person.Name, 100); writer.Write(nameBuffer); writer.Write(person.Age); writer.Write(person.Weight); } }
Вспомогательная функция CopyBytes
создает массив ровно на 100 байт. После записи мы получим файл размером 112 байт. Так как нам известен формат файла, то мы можем таким образом записать в файл, например, не один, а 15 объектов и, при необходимости, получить доступ к любому из объектов.
Так как строка само по себе хранит свою длину, то, в принципе, представленный выше пример можно упростить и не создавать массив на 100 байт, а писать в файл сразу строку. Правда, при этом, на каждый объект будет выделяться различное количество памяти. Например, объект представленный выше, если его записать в файл следующим образом:
string fileName = @"c:\CSharp Output\TextFile.dat"; using FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate); using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8, false); writer.Write(person.Name); writer.Write(person.Age); writer.Write(person.Weight);
Будет занимать не 112 байт, как ранее, а всего 51, но, при этом мы уже не сможем точно позиционировать курсор на необходимом нам объекте, если будем записывать в файл несколько объектов одного класса.
Чтение бинарных файлов в C# (класс BinaryReader)
Для чтения бинарного файла мы можем использовать класс BinaryReader
, которые, как и предыдущий класс BinaryWriter
содержит ряд специальных методов, но только для чтения данных из файла. Конструкторы класса BinaryReader
— точная копия конструкторов BinaryWriter
:
Binary |
Инициализирует новый экземпляр класса BinaryReader на основании указанного потока с использованием кодировки UTF-8. |
Binary |
Инициализирует новый экземпляр класса BinaryReader на основе указанного потока и кодировки символов. |
Binary |
Инициализирует новый экземпляр класса BinaryReader на основании указанного потока и кодировки символов, а также при необходимости оставляет поток открытым. |
Основные методы для чтения бинарных данных представлены в таблице ниже:
Метод | Описание |
---|---|
Fill |
Заполняет внутренний буфер указанным количеством байтов, которые были cчитаны из потока. |
Peek |
Возвращает следующий доступный для чтения символ, не перемещая позицию байта или символа вперед. |
Read() |
Выполняет чтение знаков из базового потока и перемещает текущую позицию в потоке вперед в соответствии с используемым значением Encoding и конкретным знаком в потоке, чтение которого выполняется в настоящий момент. |
Read(Byte[], Int32, Int32) |
Считывает указанное количество байтов из потока, начиная с заданной точки в массиве байтов. |
Read(Char[], Int32, Int32) |
Считывает указанное количество символов из потока, начиная с заданной точки в массиве символов. |
Read7Bit |
Считывает 32-разрядное целое число в сжатом формате. |
Read |
Считывает значение Boolean из текущего потока и перемещает текущую позицию в потоке на один байт вперед. |
Read |
Считывает из текущего потока следующий байт и перемещает текущую позицию в потоке на один байт вперед. |
Read |
Считывает указанное количество байтов из текущего потока в массив байтов и перемещает текущую позицию на это количество байтов. |
Read |
Считывает следующий знак из текущего потока и изменяет текущую позицию в потоке в соответствии с используемым значением Encoding и конкретным знаком в потоке, чтение которого выполняется в настоящий момент. |
Read |
Считывает указанное количество символов из текущего потока, возвращает данные в массив символов и перемещает текущую позицию в соответствии с используемой Encoding и определенным символом, считываемым из потока. |
Read |
Считывает десятичное значение из текущего потока и перемещает текущую позицию в потоке на шестнадцать байтов вперед. |
Read |
Считывает число с плавающей запятой длиной 8 байт из текущего потока и перемещает текущую позицию в потоке на восемь байт вперед. |
Read |
Считывает целое число со знаком длиной 2 байта из текущего потока и перемещает текущую позицию в потоке на два байта вперед. |
Read |
Считывает целое число со знаком длиной 4 байта из текущего потока и перемещает текущую позицию в потоке на четыре байта вперед. |
Read |
Считывает целое число со знаком длиной 8 байта из текущего потока и перемещает текущую позицию в потоке на восемь байтов вперед. |
Read |
Считывает из текущего потока байт со знаком и перемещает текущую позицию в потоке на один байт вперед. |
Read |
Считывает число с плавающей запятой длиной 4 байта из текущего потока и перемещает текущую позицию в потоке на четыре байта вперед. |
Read |
Считывает строку из текущего потока. Строка предваряется значением длины строки, которое закодировано как целое число блоками по семь битов. |
Read |
Считывает целое число без знака длиной 2 байта в формате с прямым порядком байтов из текущего потока и перемещает текущую позицию в потоке на два байта вперед. |
Read |
Считывает целое число без знака длиной 4 байта из текущего потока и перемещает текущую позицию в потоке на четыре байта вперед. |
Read |
Считывает целое число без знака длиной 8 байт из текущего потока и перемещает текущую позицию в потоке на восемь байтов вперед. |
Чтобы прочитать наш файл, созданный без использования буфера на 100 байт, можно использовать следующий код:
using BinaryReader reader = new(fs, Encoding.UTF8, false); Person person2 = new(); person.Name = reader.ReadString(); person.Age = reader.ReadInt32(); person.Weight = reader.ReadDouble(); //выводим полученные данный в консоль Console.WriteLine(person.Name); Console.WriteLine(person.Age); Console.WriteLine(person.Weight);
Если же мы будем придерживаться заданного формата и записывать имя как массив байт, то чтение очередного объекта из файла можно произвести следующим образом:
class Program { static string ReadString(byte[] buffer) { int i = buffer.Length - 1; while (buffer[i] == 0) --i; byte[] data = new byte[i + 1]; Array.Copy(buffer, data, i + 1); return Encoding.Default.GetString(data); } static void Main(string[] args) { byte[] readBuf = new byte[100]; using BinaryReader reader = new(fs, Encoding.UTF8, false); reader.Read(readBuf, 0, 100); //читаем массив байт, содержащих имя Person person2 = new(); person.Name = ReadString(readBuf); //считываем из массива имя person.Age = reader.ReadInt32(); person.Weight = reader.ReadDouble(); } }
Так как при записи массива с именем в файл все «пустые» элементы массива «забиваются» нулями, то здесь метод ReadString()
определяет последний значащий (не равный нулю) символ в переданном ему массиве и создает из полученного массива строку в заданной кодировке.
Произвольный доступ к объектам в бинарном файле
В заключении рассмотрим пример того, как, используя заданный формат бинарного файла можно получить доступ к произвольному объекту в файле. Полный исходный код приложения:
using System.Text; namespace HelloWorld { public class Person { public string Name { get; set; } public int Age { get; set; } public double Weight { get; set; } } class Program { static byte[] CopyBytes(string value, int bufferSize) { byte[] result = new byte[bufferSize]; byte[] buffer = Encoding.Default.GetBytes(value); Array.Copy(buffer, result, buffer.Length < bufferSize ? buffer.Length : bufferSize); return result; } static string ReadString(byte[] buffer) { int i = buffer.Length - 1; while (buffer[i] == 0) --i; byte[] data = new byte[i + 1]; Array.Copy(buffer, data, i + 1); return Encoding.Default.GetString(data); } static void WriteObjectToFile(BinaryWriter writer, Person data) { byte[] nameBuffer = CopyBytes(data.Name, 100); writer.Write(nameBuffer); writer.Write(data.Age); writer.Write(data.Weight); } static Person ReadObjectFromFile(BinaryReader reader, int num) { //каждый объект занимает 112 байт - определяем на какую позицию переместить курсор в потоке reader.BaseStream.Position = 112 * (num - 1); byte[] readBuf = new byte[100]; reader.Read(readBuf, 0, 100); //читаем массив байт, содержищих имя Person person = new() { Name = ReadString(readBuf), //считываем из массива имя Age = reader.ReadInt32(), Weight = reader.ReadDouble() }; return person; } static void Main(string[] args) { string fileName = @"c:\CSharp Output\TextFile.dat"; //создаем массив объектов Person[] people = new Person[] { new() { Name = "Иван Иванович Иванов", Age = 45, Weight = 70.5}, new() { Name = "Петров Петр Петрович", Age = 37, Weight = 65}, new() { Name = "Сидорова Анна Михайловна", Age = 30, Weight = 60} }; //записываем все объекты в файл using FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate); { using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8, false); { foreach (Person data in people) { WriteObjectToFile(writer, data); } } } using FileStream fs_read = new FileStream(fileName, FileMode.OpenOrCreate); { using BinaryReader reader = new(fs_read, Encoding.UTF8, false); { long count = fs_read.Length / 112; Console.WriteLine($"Количество объектов в файле: {count}"); Console.WriteLine("Введите номер объекта, который необходимо прочитать"); int num = int.Parse(Console.ReadLine()); if (num > count) throw new Exception("Задан номер больше, чем есть объектов в файле"); Person person = ReadObjectFromFile(reader, num); Console.WriteLine("----Полученный объект----"); Console.WriteLine(person.Name); Console.WriteLine(person.Age); Console.WriteLine(person.Weight); } } } } }
Рассмотрим это пример подробно. Во-первых, здесь мы записываем в файл массив из трех объектов типа Person
. Запись производится в методе WriteObjectToFile
:
static void WriteObjectToFile(BinaryWriter writer, Person data) { byte[] nameBuffer = CopyBytes(data.Name, 100); writer.Write(nameBuffer); writer.Write(data.Age); writer.Write(data.Weight); }
Код метода CopyBytes
был представлен выше. После того, как файл записан, а все ресурсы, используемые объектом BinaryWriter
и потоком FileStream
освобождены, мы создаем объект типа BinaryReader
для чтения файла и первым делом определяем количество объектов в файле:
using FileStream fs_read = new FileStream(fileName, FileMode.OpenOrCreate); { using BinaryReader reader = new(fs_read, Encoding.UTF8, false); { long count = fs_read.Length / 112; Console.WriteLine($"Количество объектов в файле: {count}"); Console.WriteLine("Введите номер объекта, который необходимо прочитать"); .... }
Так как размер, занимаемый одним объектом нам точно известен (112 байт), то рассчитать количество объектов в файле не составляет никакого труда. Далее, мы просим пользователя ввести номер объекта который необходимо прочитать. Если номер оказывается допустимым (он менее или равен количеству объектов в файле), то вызывается метод чтения конкретного объекта из файла — ReadObjectFromFile
:
static Person ReadObjectFromFile(BinaryReader reader, int num) { //каждый объект занимает 112 байт - определяем на какую позицию переместить курсор в потоке reader.BaseStream.Position = 112 * (num - 1); byte[] readBuf = new byte[100]; reader.Read(readBuf, 0, 100); //читаем массив байт, содержащих имя Person person = new() { Name = ReadString(readBuf), //считываем из массива имя Age = reader.ReadInt32(), Weight = reader.ReadDouble() }; return person; }
здесь мы устанавливаем курсор в потоке на необходимую позицию и последовательно считываем все поля объекта. После того, как объект прочитан — выводим его пользователю в консоль. Результат может быть, например, таким:
Введите номер объекта, который необходимо прочитать
3
—-Полученный объект—-
Сидорова Анна Михайловна
30
60
Представленный выше пример, конечно, можно улучшить, определить более экономичный, с точки зрения потребления места на диске, формат и так далее. Здесь же я просто привел один из вариантов того, как можно осуществить произвольный доступ к объектам, записанным в бинарном файле, используя BinaryReader
.
Итого
Для доступа к данным в бинарных файлах удобно использовать классы BinaryWriter
и BinaryReader
. Эти классы содержат специальные методы, позволяющие без дополнительных преобразований записывать и считывать строки, числа и другие данные из файлов. Используя заданный формат бинарного файла можно организовать произвольный доступ к различным частям бинарного файла для считывания/записи данных.