Запись и чтение структур (бинарные файлы)

В предыдущем разделе мы научились выполнять операции ввода-вывода над массивами структур. Когда чтение или запись касается отдельной структуры, удобнее воспользоваться парой функций FileWriteStruct и FileReadStruct.

uint FileWriteStruct(int handle, const void &data, int size = -1)

Функция записывает в бинарный файл с дескриптором handle содержимое "простой" структуры data. Как мы знаем, такие структуры могут содержать только поля встроенных нестроковых типов и вложенные "простые" структуры.

Основная "фишка" функции заключается в параметре size. С помощью него задается количество записываемых байтов, что позволяет отбросить некоторую часть структуры (её окончание). По умолчанию параметр равен -1, что означает сохранение всей структуры целиком. Если size больше размера структуры, превышение игнорируется, то есть записывается только структура, sizeof(data) байтов.

При успешном выполнении функция возвращает количество записанных байтов, в случае ошибки — 0.

uint FileReadStruct(int handle, void &data, int size = -1)

Функция считывает из бинарного файла с дескриптором handle содержимое в структуру data. Параметр size позволяет указать количество байтов, подлежащих чтению. Если оно не указано или превышает размер структуры, то используется точный размер указанной структуры.

При успешном выполнении функция возвращает количество прочитанных байтов, в случае ошибки — 0.

Опция с отсечением концовки структуры присутствует только в функциях FileWriteStruct и FileReadStruct. Поэтому их использование в цикле становится наиболее подходящей альтернативой для сохранения и чтения массива "урезанных" структур: функции FileWriteArray и FileReadArray такой возможностью не обладают, а запись и чтение по отдельным полям более накладно (мы рассмотрим соответствующие функции в следующих разделах).

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

Рассмотрим примеры использования этих двух функций в скрипте FileStruct.mq5.

Предположим, мы хотим периодически архивировать последние котировки, чтобы иметь возможность проверять их неизменность в дальнейшем или сравнивать с аналогичными периодами у других поставщиков. В принципе, это можно сделать вручную через диалог "Символы" (закладка "Бары") в MetaTrader 5, но потребовало бы лишних усилий и соблюдения расписания. Гораздо проще делать это автоматически из программы. Кроме того, ручной экспорт котировок делается в текстовом формате CSV, а нам может потребоваться отправлять файлы на внешний сервер и потому желательно сохранять их в компактном двоичном виде. В дополнение к этому допустим, что информация о тиках, спреде и реальных объемах (которые всегда пусты для символов Forex) нас не интересует.

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

С помощью параметра size в FileWriteStruct мы сможем сохранять лишь часть структуры MqlRates, без последних полей.

В начале скрипта определим макросы и имя тестового файла.

#define BARLIMIT 10 // количество баров для записи
#define HEADSIZE 10 // размер заголовка нашего формата
const string filename = "MQL5Book/struct.raw";

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

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

Для наших целей была разработана простая структура FileHeader.

struct FileHeader
{
   uchar signature[HEADSIZE];
   int n;
   FileHeader(const int size = 0) : n(size)
   {
      static uchar s[HEADSIZE] = {'C','A','N','D','L','E','S','1','.','0'};
      ArrayCopy(signatures);
   }
};

Она начинается с текстовой сигнатуры "CANDLES" (в поле signature), номера версии "1.0" (там же) и количества записей (в поле n). Поскольку мы не можем использовать строковое поле для сигнатуры (тогда структура перестала бы быть простой и отвечающей требованиям файловых функций), текст фактически упакован в массив uchar фиксированного размера HEADSIZE. Его инициализацию в экземпляре делает конструктор на основе локальной статической копии.

В функции OnStart запрашиваем BARLIMIT последних баров, открываем файл в режиме FILE_WRITE и записываем в него сперва заголовок, а потом полученные котировки в усеченном виде.

void OnStart()
{
   MqlRates rates[], candles[];
   int n = PRTF(CopyRates(_Symbol_Period0BARLIMITrates)); // 10 / ok
   if(n < 1return;
  
   // создаем новый файл или перезаписываем с нуля старый
   int handle = PRTF(FileOpen(filenameFILE_BIN | FILE_WRITE)); // 1 / ok
  
   FileHeader fh(n);// заголовок с актуальным количеством записей
  
   // сначала записываем заголовок
   PRTF(FileWriteStruct(handlefh)); // 14 / ok
  
   // потом записываем данные
   for(int i = 0i < n; ++i)
   {
      FileWriteStruct(handlerates[i], offsetof(MqlRatestick_volume));
   }
   FileClose(handle);
   ArrayPrint(rates);
   ...

В качестве значения параметра size функции FileWriteStruct используется выражение со знакомым нам оператором offsetof: offsetof(MqlRates, tick_volume), то есть все поля начиная с tick_volume отбрасываются при записи в файл.

Для проверочного чтения данных откроем тот же файл в режиме FILE_READ и прочитаем структуру FileHeader.

   handle = PRTF(FileOpen(filenameFILE_BIN | FILE_READ)); // 1 / ok
   FileHeader referencereader;
   PRTF(FileReadStruct(handlereader)); // 14 / ok
   // если заголовки не совпадают, это не наши данные
   if(ArrayCompare(reader.signaturereference.signature))
   {
      Print("Wrong file format; 'CANDLES' header is missing");
      return;
   }

В структуре reference содержится неизмененный заголовок по умолчанию (сигнатура). В структуру reader попало 14 байтов из файла. Если две сигнатуры совпали, мы можем продолжать работу, так как формат файла оказался правильным, и в поле reader.n находится прочитанное из файла количество записей. Мы выделяем и обнуляем память требуемого размера под приемный массив candles, а затем читаем в него все записи.

   PrintFormat("Reading %d candles..."reader.n);
   ArrayResize(candlesreader.n); // распределяем память под ожидаемые данные заранее
   ZeroMemory(candles);
   
   for(int i = 0i < reader.n; ++i)
   {
      FileReadStruct(handlecandles[i], offsetof(MqlRatestick_volume));
   }
   FileClose(handle);
   ArrayPrint(candles);
}

Обнуление потребовалось потому, что чтение структур MqlRates также производится частично, и без обнуления оставшиеся поля содержали бы "мусор".

Посмотрим в журнале, какими были исходные данные (целиком) для XAUUSD,H1.

                 [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]

[0] 2021.08.16 03:00:00 1778.86 1780.58 1778.12 1780.56          3049        5             0

[1] 2021.08.16 04:00:00 1780.61 1782.58 1777.10 1777.13          4633        5             0

[2] 2021.08.16 05:00:00 1777.13 1780.25 1776.99 1779.21          3592        5             0

[3] 2021.08.16 06:00:00 1779.26 1779.26 1776.67 1776.79          2535        5             0

[4] 2021.08.16 07:00:00 1776.79 1777.59 1775.50 1777.05          2052        6             0

[5] 2021.08.16 08:00:00 1777.03 1777.19 1772.93 1774.35          3213        5             0

[6] 2021.08.16 09:00:00 1774.38 1775.41 1771.84 1773.33          4527        5             0

[7] 2021.08.16 10:00:00 1773.26 1777.42 1772.84 1774.57          4514        5             0

[8] 2021.08.16 11:00:00 1774.61 1776.67 1773.69 1775.95          3500        5             0

[9] 2021.08.16 12:00:00 1775.96 1776.12 1773.68 1774.44          2425        5             0

А теперь посмотрим, что было прочитано.

                 [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]

[0] 2021.08.16 03:00:00 1778.86 1780.58 1778.12 1780.56             0        0             0

[1] 2021.08.16 04:00:00 1780.61 1782.58 1777.10 1777.13             0        0             0

[2] 2021.08.16 05:00:00 1777.13 1780.25 1776.99 1779.21             0        0             0

[3] 2021.08.16 06:00:00 1779.26 1779.26 1776.67 1776.79             0        0             0

[4] 2021.08.16 07:00:00 1776.79 1777.59 1775.50 1777.05             0        0             0

[5] 2021.08.16 08:00:00 1777.03 1777.19 1772.93 1774.35             0        0             0

[6] 2021.08.16 09:00:00 1774.38 1775.41 1771.84 1773.33             0        0             0

[7] 2021.08.16 10:00:00 1773.26 1777.42 1772.84 1774.57             0        0             0

[8] 2021.08.16 11:00:00 1774.61 1776.67 1773.69 1775.95             0        0             0

[9] 2021.08.16 12:00:00 1775.96 1776.12 1773.68 1774.44             0        0             0

Котировки совпадают, но последние три поля в каждой структуре пусты.

Желающие могут перейти в папку MQL5/Files/MQL5Book и изучить внутреннее представление файла struct.raw (используйте программу просмотра, поддерживающую двоичный режим; пример показан ниже).

Варианты представления бинарного файла с архивом котировок во внешней программе просмотра

Варианты представления бинарного файла с архивом котировок во внешней программе просмотра

Здесь представлен типичный способ отображения бинарных файлов: слева расположена колонка с адресами (смещениями от начала файла), в середине — коды байтов, а справа — символьное представление соответствующих байтов. Первая и вторая колонки используют шестнадцатеричную запись чисел. Символы в правой колонке могут отличаться, в зависимости от выбранной кодовой страницы ANSI. На них имеет смысл обращать внимание только в тех фрагментах, где известно наличие текста. В нашем случае наглядно "проявляется" сигнатура "CANDLES1.0" в самом начале. Числа следует анализировать по средней колонке. В ней, например, после сигнатуры видно 4-байтовое значение 0x0A000000, то есть 0x0000000A в "перевернутом" виде (вспоминаем раздел Управление порядком байтов в целых числах): это 10 — количество записанных структур.