Запись и чтение файлов в упрощенном режиме

Среди файловых функций MQL5, которые предназначены для записи и чтения данных, существует разделение на 2 неравных группы. В первую из них входят две функции — FileSave и FileLoad, позволяющие записывать или считывать данные в двоичном режиме за один вызов функции. С одной стороны, данный подход имеет неоспоримое преимущество — простоту, но с другой — имеет некоторые ограничения (подробнее о них — чуть ниже). Во второй многочисленной группе все файловые функции используются иначе: требуется последовательно вызвать несколько из них, чтобы выполнить логически законченную операцию чтения или записи. Это кажется более сложным, но зато предоставляет гибкость и управление процессом. Функции второй групп оперируют особыми целыми числами — дескрипторами файлов, которые следует получить с помощью функции FileOpen (см. следующий раздел).

Познакомимся с формальным описанием этих двух функций, а затем рассмотрим их совместный пример (FileSaveLoad.mq5).

bool FileSave(const string filename, const void &data[], const int flag = 0)

Функция записывает в бинарный файл с именем filename все элементы переданного массива data. Параметр filename может содержать не только имя файла, но и имена папок нескольких уровней вложенности: функция создаст указанные папки, если их еще нет. Если файл существует, он будет перезаписан (если не занят другой программой).

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

Параметр flag может, при необходимости, содержать предопределенную константу FILE_COMMON, которая означает создание и запись файла в общий каталог данных всех терминалов (Common/Files/). Если флаг не указан (что соответствует значению по умолчанию 0), то файл пишется в обычный каталог данных (если MQL-программа выполняется в терминале) или в каталог агента тестирования (если дело происходит в тестере). В двух последних случаях, как было описано в начале главы, внутри каталога используется "песочница" MQL5/Files/.

Функция возвращает признак успеха операции (true) или ошибки (false).

long FileLoad(const string filename, void &data[], const int flag = 0)

Функция считывает всё содержимое бинарного файла filename в указанный массив data. Имя файла может включать иерархию папок внутри "песочницы" MQL5/Files или Common/Files.

Массив data должен иметь любой встроенный тип кроме string, или тип простой структуры (см. выше).

Параметр flag управляет выбором каталога, где ищется и открывается файл: по умолчанию (при значении 0) — в стандартной "песочнице", а если задано значение FILE_COMMON, то — в общей для всех терминалов.

Функция возвращает количество прочитанных элементов или -1 в случае ошибки.

Обратите внимание, что данные из файла читаются блоками размером в один элемент массива. Если размер файла оказывается некратным размеру элемента, то оставшиеся данные пропускаются (не читаются). Например, при размере файла 10 байтов его чтение в массив типа double (sizeof(double)=8) приведет к тому, что фактически будет загружено только 8 байтов, то есть 1 элемент (и функция вернет 1). Оставшиеся 2 байта в конце файла будут проигнорированы.

В скрипте FileSaveLoad.mq5 определим две структуры для тестов.

struct Pair
{
   short xy;
};
  
struct Simple
{
   double d;
   int i;
   datetime t;
   color c;
   uchar a[10]; // массив фиксированного размера разрешен
   bool b;
   Pair p;      // составные поля (вложенные простые структуры) тоже разрешены
   
   // строки и динамические массивы вызовут ошибку компиляции при использовании
   // FileSave/FileLoad: structures or classes containing objects are not allowed
   // string s;
   // uchar a[];
   
   // указатели также не поддерживаются
   // void *ptr;
};

Структура Simple содержит поля большинства разрешенных типов, а также составное поле с типом структуры Pair. В функции OnStart заполним небольшой массив типа Simple.

void OnStart()
{
   Simple write[] =
   {
      {+1.0, -1D'2021.01.01', clrBlue, {'a'}, true, {100016000}},
      {-1.0, -2D'2021.01.01', clrRed,  {'b'}, true, {100016000}},
   };
   ...

Файл для записи данных выберем вместе с вложенной папкой "MQL5Book", чтобы наши эксперименты не перемешивались с вашими рабочими файлами:

   const string filename = "MQL5Book/rawdata";

Запишем массив в файл, прочитаем его в другой массив и сравним их.

   PRT(FileSave(filenamewrite/*, FILE_COMMON*/)); // true
   
   Simple read[];
   PRT(FileLoad(filenameread/*, FILE_COMMON*/)); // 2
   
   PRT(ArrayCompare(writeread)); // 0

FileLoad вернула 2, то есть было прочитано 2 элемента (2 структуры). Результат сравнения 0 означает, что данные совпали. Вы можете в своем любимом файловом менеджере открыть папку MQL5/Files/MQL5Book и убедиться, что там есть файл "rawdata" (смотреть его внутренности текстовым редактором не рекомендуется, используйте программу просмотра с поддержкой двоичного режима).

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

   uchar bytes[];
   for(int i = 0i < ArraySize(read); ++i)
   {
      uchar temp[];
      PRT(StructToCharArray(read[i], temp));
      ArrayCopy(bytestempArraySize(bytes));
   }
   ByteArrayPrint(bytes);

Результат:

 [00] 00 | 00 | 00 | 00 | 00 | 00 | F0 | 3F | FF | FF | FF | FF | 00 | 66 | EE | 5F | 
 [16] 00 | 00 | 00 | 00 | 00 | 00 | FF | 00 | 61 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 
 [32] 00 | 00 | 01 | E8 | 03 | 80 | 3E | 00 | 00 | 00 | 00 | 00 | 00 | F0 | BF | FE | 
 [48] FF | FF | FF | 00 | 66 | EE | 5F | 00 | 00 | 00 | 00 | FF | 00 | 00 | 00 | 62 | 
 [64] 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 01 | E8 | 03 | 80 | 3E | 

Поскольку встроенная функция ArrayPrint не умеет "печатать" в шестнадцатеричном формате, нам пришлось разработать собственную функцию ByteArrayPrint (здесь её исходный код не будем приводить, см. прилагаемый файл).

Далее, вспомним, что FileLoad способна загрузить данные в массив любого типа, поэтому прочтем тот же файл с помощью неё непосредственно в массив байтов.

   uchar bytes2[];
   PRT(FileLoad(filenamebytes2/*, FILE_COMMON*/)); // 78,  39 * 2
   PRT(ArrayCompare(bytesbytes2)); // 0, равенство

Успешное сравнение двух байтовых массивов показывает, что FileLoad "не стесняется" оперировать сырыми данными из файла произвольным образом: так, как ему "скажут" (в файле нет информации о том, что он хранит массив именно структур Simple).

Здесь важно отметить, что поскольку тип байт имеет минимальный размер (1), то ему кратен любой размер файла. Поэтому в байтовый массив любой файл всегда читается без остатка. Функция FileLoad вернула здесь число 78 (число элементов равно числу байтов). Это размер файла (две структуры по 39 байтов каждая).

В принципе, способность FileLoad интерпретировать данные под любой тип требует осторожности и проверок со стороны программиста. В частности, далее в скрипте мы читаем тот же файл в массив структур MqlDateTime. Это, конечно же, неправильно, но работает без ошибок.

   MqlDateTime mdt[];
   PRT(sizeof(MqlDateTime)); // 32
   PRT(FileLoad(filenamemdt)); // 2
   // внимание: 14 байтов осталось непрочитанными
   ArrayPrint(mdt);

Результат содержит бессмысленный набор чисел:

        [year]      [mon] [day]     [hour]    [min]    [sec] [day_of_week] [day_of_year]
[0]          0 1072693248    -1 1609459200        0 16711680            97             0
[1] -402587648    4096003     0  -20975616 16777215  6286950     -16777216    1644167168

Поскольку размер MqlDateTime равен 32, то в файле длиной 78 байтов укладывается только две таких структуры, причем лишними остаются еще 14 байтов. Само наличие остатка свидетельствует о проблеме. Но даже если его нет, это не гарантирует осмысленность выполненной операции, потому что два разных размера могут чисто случайно укладываться целое (но разное) число раз в длину файла. Более того, две разных по смыслу структуры могут иметь одинаковый размер, но это не значит, что их следует записывать и читать из одной в другую.

Неудивительно, что вывод в журнал массива структур MqlDateTime показывает "сумасшедшие" величины: ведь это был, на самом деле, совсем другой тип данных.

Чтобы сделать чтение в некоторой степени более "осторожным", в скрипте реализован аналог функции FileLoadMyFileLoad. Детально мы разберем эту функцию, а также парную ей MyFileSave, в следующих разделах, когда изучим новые файловые функции и с их помощью смоделируем внутреннее устройство FileSave/FileLoad. А пока лишь отметим, что в своей версии мы можем проверять наличие непрочитанного остатка в файле и выводить предупреждение.

В заключение разберем еще пару потенциальных ошибок, продемонстрированных в скрипте.

   /*
   // ошибка компиляции, тип string не поддерживается здесь
   string texts[];
   FileSave("any", texts); // parameter conversion not allowed
   */
   
   double data[];
   PRT(FileLoad("any"data)); // -1
   PRT(_LastError); // 5004, ERR_CANNOT_OPEN_FILE

Первая из них происходит во время компиляции (в связи с чем блок кода закомментирован), потому что строковые массивы запрещены.

Вторая заключается в чтении несуществующего файла, из-за чего FileLoad возвращает -1. Пояснительный код ошибки легко получить с помощью GetLastError (или _LastError).