Запись и чтение массивов

Две функции MQL5 предназначены для записи и чтения массивов: FileWriteArray и FileReadArray. С двоичными файлами они позволяют обрабатывать массивы любых встроенных типов кроме строк, а также массивы простых структур, в которых нет строковых полей, объектов, указателей и динамических массивов. Данные ограничения связаны с оптимизацией процессов записи и чтения, которая возможна за счет исключения типов с переменной длиной. А таковыми как раз и являются строки, объекты и динамические массивы.

Вместе с тем, при работе с текстовыми файлами данные функции способны оперировать массивами типа string (прочие типы массивов в файлах с режимом FILE_TXT/FILE_CSV не допускаются этими функциями). Такие массивы хранятся в файле в формате: по одному элементу на каждой строке.

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

Кроме того, поддержку структур со строками можно организовать за счет внутренней оптимизации хранения информации. Например, вместо строковых полей можно применить целочисленные, которые будут содержать индексы соответствующих строк в отдельном массиве со строками. Учитывая возможность переопределить средствами ООП многие операции (в частности, присваивание) и получение структурного элемента массива по номеру, внешний вид алгоритма практически не изменится. Зато при записи можно сначала открыть файл в двоичном режиме и вызвать FileWriteArray для массива с "упрощенным" типом структур, а затем переоткрыть файл в текстовом режиме и дописать в него массив всех строк с помощью второго вызова FileWriteArray. Для чтения такого файла следует предусмотреть в его начале некий заголовок, содержащий количество элементов в массивах, чтобы передать его в качестве параметра count в FileReadArray (см. далее).  

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

Изучим сигнатуры функций, а потом рассмотрим общий пример (FileArray.mq5).

uint FileWriteArray(int handle, const void &array[], int start = 0, int count = WHOLE_ARRAY)

Функция записывает в файл с дескриптором handle массив array, который может быть многомерным. Параметры start и count позволяют задать диапазон элементов, по умолчанию он равен всему массиву. В случае многомерных массивов индекс start и количество элементов count относятся к сквозной нумерации по всем измерениям, а не к первому измерению массива. Например, если массив имеет конфигурацию [][5], то значение start, равное 7, укажет на элемент с индексами [1][2], а count = 2 добавит к нему элемент [1][3].

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

Если handle получен в двоичном режиме, массивы могут быть любых встроенных типов, кроме строк, или типов простых структур. Если handle открыт в любом из текстовых режимов, массив должен быть типа string.

uint FileReadArray(int handle, const void &array[], int start = 0, int count = WHOLE_ARRAY)

Функция читает из файла с дескриптором handle данные в массив. Массив может быть многомерным и динамическим. Для многомерных массивов параметры start и count работают по принципу сквозной нумерации элементов по всем измерениям, описанному выше. Динамический массив при необходимости автоматически увеличивается в размере под читаемые данные. Если start больше исходной длины массива, эти промежуточные элементы после выделения памяти будут содержать случайные данные (см. пример).

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

Если handle получен в двоичном режиме, массивы могут быть любых встроенных нестроковых типов или типов простых структур. Если handle открыт в текстовом режиме, массив должен быть типа string.

В скрипте FileArray.mq5 проверим работу как в бинарном, так и в текстовом режиме. Для этого зарезервируем два имени файлов.

const string raw = "MQL5Book/array.raw";
const string txt = "MQL5Book/array.txt";

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

void OnStart()
{
   long numbers1[][2] = {{14}, {25}, {36}};
   long numbers2[][2];
   long numbers3[][2];
   
   string text1[][2] = {{"1.0""abc"}, {"2.0""def"}, {"3.0""ghi"}};
   string text2[][2];
   ...

Кроме того, для тестирования работы со структурами определены 3 следующих типа:

struct TT
{
   string s1;
   string s2;
};
  
struct B
{
private:
   int b;
public:
   void setB(const int v) { b = v; }
};
  
struct XYZ : public B
{
   color xyz;
};

Структуру типа TT мы не сможем использовать в описываемых функциях, поскольку она содержит строковые поля. Она нужна, что продемонстрировать потенциальную ошибку компиляции в закомментированной инструкции (см. далее). Наследование между структурами B и XYZ, а также наличие закрытого поля не являются препятствием для функций FileWriteArray и FileReadArray.

Структуры используются для декларации пары массивов:

   TT tt[]; // пустой, т.к. данные не важны 
   XYZ xyz[1];
   xyz[0].setB(-1);
   xyz[0].x = xyz[0].y = xyz[0].z = clrRed;

Начнем с бинарного режима. Создадим новый или откроем существующий файл, сбросив его содержимое. Затем в трех вызовах FileWriteArray попытаемся записать массивы numbers1, text1 и xyz.

   int writer = PRTF(FileOpen(rawFILE_BIN | FILE_WRITE)); // 1 / ok
   PRTF(FileWriteArray(writernumbers1)); // 6 / ok
   PRTF(FileWriteArray(writertext1)); // 0 / FILE_NOTTXT(5012)
   PRTF(FileWriteArray(writerxyz)); // 1 / ok
   FileClose(writer);
   ArrayPrint(numbers1);

Массивы numbers1 и xyz записываются успешно, о чем говорит количество записанных элементов. Массив text1 получает отказ с ошибкой FILE_NOTTXT(5012) из-за того, что строковые массивы требуют, чтобы файл был открыт в текстовом режиме. Поэтому содержимое xyz будет расположено в файле непосредственно после всех элементов numbers1.

Обратите внимание, каждая функция записи (или чтения) начинает записывать (или считывать) данные в текущую позицию внутри файла и сдвигает её на размер записанных или прочитанных данных. Если этот указатель находится в конце файла перед операцией записи, размер файла увеличивается. Если конец файла достигается в процессе чтения, указатель больше не двигается, а система взводит специальный внутренний код ошибки 5027 (FILE_ENDOFFILE). В новом файле нулевого размера начало и конец совпадают.

Из массива text1 было записано 0 элементов, поэтому ничто в файле не напоминает о том, что между двумя удачными вызовами FileWriteArray был один неудачный.

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

Прочитаем файл в массив numbers2.

   int reader = PRTF(FileOpen(rawFILE_BIN | FILE_READ)); // 1 / ok
   PRTF(FileReadArray(readernumbers2)); // 8 / ok
   ArrayPrint(numbers2);

Поскольку в файл было записано два разных массива (не только numbers1, но и xyz), в приемный массив прочиталось 8 элементов (т.е. весь файл до конца, т.к. иное не было задано с помощью параметров).

Действительно, размер структуры XYZ равен 16 байтам (4 поля по 4 байта: один int и три color), что соответствует одному ряду в массиве numbers2 (2 элемента типа long). В данном случае, это совпадение. Как уже отмечалось выше, функции не имеют представления о конфигурации и размере сырых данных, и могут прочитать все что угодно в какой угодно массив: за корректностью операции должен следить программист.

Сравним исходное и полученное состояния. Исходный массив numbers1:

       [,0][,1]
   [0,]   1   4
   [1,]   2   5
   [2,]   3   6

Полученный массив numbers2:

                 [,0]          [,1]
   [0,]             1             4
   [1,]             2             5
   [2,]             3             6
   [3,] 1099511627775 1095216660735

Начало массива numbers2 полностью совпадает с исходным массивом numbers1, то есть запись и чтение через файл работают исправно.

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

Теперь "перемотаем" файл на начало (это делается с помощью функции FileSeek, которую мы рассмотрим позднее, в разделе Управление позицией внутри файла) и вызовем FileReadArray с указанием номера и числа элементов, то есть выполним частичное чтение.

   PRTF(FileSeek(reader0SEEK_SET)); // true
   PRTF(FileReadArray(readernumbers3103));
   FileClose(reader);
   ArrayPrint(numbers3);

Из файла считываются 3 элемента и помещаются, начиная с индекса 10, в приемный массив numbers3. Поскольку файл читается с начала, этими элементами являются значения 1, 4, 2. А поскольку двумерный массив имеет конфигурацию [][2], сквозной индекс 10 указывает на элемент [5,0]. Вот как это выглядит в памяти:

       [,0][,1]
   [0,]   1   4
   [1,]   1   4
   [2,]   2   6
   [3,]   0   0
   [4,]   0   0
   [5,]   1   4
   [6,]   2   0

Элементы, помеченные желтым цветом, являются случайными (могут меняться от запуска к запуску скрипта). Вполне может быть, что они все окажутся нулевыми, но это не гарантировано. Изначально массив numbers3 был пустым, и вызов FileReadArray инициировал распределение памяти, достаточной для приема 3 элементов по смещению 10 (итого 13). Выделенный блок ничем не заполняется, а из файла читается только 3 числа. Поэтому элементы со сквозными индексами от 0 до 9 (т.е. 5 первых рядов), а также последний, с индексом 13, содержат "мусор".

Напомним, что многомерные массивы "масштабируются" по первому измерению, и потому прирост в 1 номер означает добавление всей конфигурации по высшим измерениям. В данном случае, распределение касается ряда из двух чисел ([][2]). Иными словами, запрошенный размер 13 округлен в большую сторону до кратного двум, то есть 14.

Наконец, протестируем работу функций со строковыми массивами. Создадим новый или откроем существующий файл, сбросив его содержимое. Затем в двух вызовах FileWriteArray попытаемся записать массивы text1 и numbers1.

   writer = PRTF(FileOpen(txtFILE_TXT | FILE_ANSI | FILE_WRITE)); // 1 / ok
   PRTF(FileWriteArray(writertext1)); // 6 / ok
   PRTF(FileWriteArray(writernumbers1)); // 0 / FILE_NOTBIN(5011)
   FileClose(writer);

Строковый массив сохраняется успешно. Числовой — игнорируется с ошибкой FILE_NOTBIN(5011), поскольку для него файл должен быть открыт в двоичном режиме.

При попытке записать массив структур tt мы получим ошибку компиляции с пространным сообщением "структуры или классы с объектами не разрешены". На самом деле компилятор имеет в виду, что ему не нравятся поля типа string (подразумевается, что строки и динамические массивы имеют внутреннее представление неких служебных объектов). Таким образом, несмотря на то, что файл открыт в текстовом режиме, и в структуре — только текстовые поля, такое сочетание не поддерживается MQL5.

   // ОШИБКА КОМПИЛЯЦИИ: structures or classes containing objects are not allowed
   FileWriteArray(writertt);

Наличие строковых полей делает структуру "сложной" и непригодной для работы с функциями FileWriteArray/FileReadArray в любом режиме.

После запуска скрипта вы можете перейти в каталог MQL5/Files/MQL5Book и изучить содержимое сгенерированных файлов.

Ранее в разделе Запись и чтение файлов в упрощенном режиме мы рассмотрели функции FileSave и FileLoad. В тестовом скрипте (FileSaveLoad.mq5) для них были реализованы, но детально не представлены эквивалентные варианты этих функций, использующие FileWriteArray и FileReadArray. Поскольку теперь мы познакомились с этими новыми функциями, то можем изучить исходный код:

template<typename T>
bool MyFileSave(const string nameconst T &array[], const int flags = 0)
{
   const int h = FileOpen(nameFILE_BIN | FILE_WRITE | flags);
   if(h == INVALID_HANDLEreturn false;
   FileWriteArray(harray);
   FileClose(h);
   return true;
}
   
template<typename T>
long MyFileLoad(const string nameT &array[], const int flags = 0)
{
   const int h = FileOpen(nameFILE_BIN | FILE_READ | flags);
   if(h == INVALID_HANDLEreturn -1;
   const uint n = FileReadArray(harray0, (int)(FileSize(h) / sizeof(T)));
   // эта версия дополнена по сравнению со стандартной FileLoad следующей проверкой:
   // если размер файла не кратен размеру структуры, выводим предупреждение
   const ulong leftover = FileSize(h) - FileTell(h);
   if(leftover != 0)
   {
      PrintFormat("Warning from %s: Some data left unread: %d bytes",
         __FUNCTION__leftover);
      SetUserError((ushort)leftover);
   }
   FileClose(h);
   return n;
}

Как видно, MyFileSave строится на единственном вызове FileWriteArray, а MyFileLoad — на вызове FileReadArray, между парой вызовов FileOpen/FileClose. В обоих случаях пишутся и читаются все имеющиеся данные. Благодаря шаблонам наши функции также способны принимать массивы произвольных типов. Но если в качестве мета-параметра T будет выведен какой-либо неподдерживаемый тип (например, класс), то возникнет ошибка компиляции, как и при некорректном обращении ко встроенным функциям.