- Способы хранения информации: текстовый и двоичный
- Запись и чтение файлов в упрощенном режиме
- Открытие и закрытие файлов
- Управление дескрипторами файлов
- Выбор кодировки для текстового режима
- Запись и чтение массивов
- Запись и чтение структур (бинарные файлы)
- Запись и чтение переменных (бинарные файлы)
- Запись и чтение переменных (текстовые файлы)
- Управление позицией внутри файла
- Получение свойств файла
- Принудительная запись кэша на диск
- Удаление и проверка на существование файла
- Копирование и перемещение файлов
- Поиск файлов и папок
- Работа с папками
- Диалог выбора файла или папки
Запись и чтение переменных (бинарные файлы)
Если какая-либо структура содержит поля тех типов, которые запрещены для "простых" структур (строки, динамические массивы, указатели), то записать её в файл или прочитать из файла рассмотренными ранее функциями не получится. То же самое касается и объектов классов. Вместе с тем, такие сущности содержат обычно большую часть данных в программах и также требуют сохранения и восстановления своего состояния.
На примере структуры заголовка в предыдущем разделе было наглядно видно, что без строк (и прочих типов переменной длины) можно, в принципе, обойтись, но при этом приходится изобретать альтернативные, более громоздкие реализации алгоритмов (например, заменять строку на массив символов).
Для записи и чтения данных произвольной сложности MQL5 предоставляет наборы функций нижнего уровня, если так можно выразиться. Они оперируют одним значением конкретного типа: double, float, int/uint, long/ulong или string. Напомним, что все другие встроенные типы MQL5 эквиваленты целым разного размера: char/uchar — 1 байт, short/ushort — 2 байта, color — 4 байта, перечисления — 4 байта, datetime — 8 байтов. Такие функции можно назвать атомарными (т.е. неделимыми), потому что функций чтения и записи в файлы на уровне битов уже не существует.
Разумеется, поэлементная запись или чтение снимает также и ограничение на файловые операции с динамическими массивами.
Что же касается указателей на объекты, в духе парадигмы ООП мы можем поручить сохранение и восстановление объектов им самим: достаточно в каждом классе реализовать некий интерфейс (набор методов), отвечающий за перенос важного содержимого в файлы и обратно, и использующий низкоуровневые функции. Тогда, если в составе объекта нам встретится поле-указатель на другой объект, мы просто делегируем сохранение или чтение ему, а он, в свою очередь, "разберется" со своими полями, среди которых могут быть другие указатели, и делегирование продолжится вглубь, пока не охватит все элементы.
Обратите внимание, что в данном разделе мы рассмотрим атомарные функции для бинарных файлов. Их аналоги для текстовых файлов будут представлены в следующем разделе. Все функции данного раздела возвращают количество записанных байтов или 0 в случае ошибки.
uint FileWriteDouble(int handle, double value)
uint FileWriteFloat(int handle, float value)
uint FileWriteLong(int handle, long value)
Функции записывают в бинарный файл с дескриптором handle значение соответствующего типа, переданное в параметре value (double, float, long).
uint FileWriteInteger(int handle, int value, int size = INT_VALUE)
Функция записывает в бинарный файл с дескриптором handle целочисленное значение value. Размер значения в байтах задается параметром size и может быть одной из предопределенных констант: CHAR_VALUE (1), SHORT_VALUE (2), INT_VALUE (4, по умолчанию), что соответствует типам char, short и int (со знаком и без знака).
Функция поддерживает недокументированный режим записи целого числа размером 3 байта. Его использование не рекомендуется.
Файловый указатель перемещается на количество записанных байтов (а не на размер int).
uint FileWriteString(int handle, const string value, int length = -1)
Функция записывает в бинарный файл с дескриптором handle строку из параметра value. В параметре length можно указать количество символов для записи. Если оно меньше длины строки, в файл попадет лишь указанная часть строки. Если length равен -1 или не указан, в файл переносится вся строка без терминального нуля. Если length больше длины строки, лишние символы заполняются в файле нулями.
Следует иметь в виду, что при записи в файл, открытый с флагом FILE_UNICODE (либо без флага FILE_ANSI), строка сохраняется в формате Unicode (каждый символ занимает 2 байта). При записи в файл, открытый с флагом FILE_ANSI, каждый символ занимает 1 байт (возможно искажение национальных символов).
Функция FileWriteString может работать и с текстовыми файлами. Такое её применение описано в следующем разделе.
double FileReadDouble(int handle)
float FileReadFloat(int handle)
long FileReadLong(int handle)
Функции читают число соответствующего типа — double, float или long — из бинарного файла с указанным дескриптором. При необходимости приведите результат к ulong (если в файле в данной позиции ожидается беззнаковое длинное целое).
int FileReadInteger(int handle, int size = INT_VALUE)
Функция читает из бинарного файла с дескриптором handle целочисленное значение, причем его размер в байтах указывается в параметре size.
Поскольку результат функции — типа int, его необходимо явно приводить к требуемому целевому типу, если он отличается от int (т.е. к uint, или short/ushort, или char/uchar). В противном случае вы, как минимум, получите предупреждение компилятора, а как максимум — потерю знака.
Дело в том, что при чтении CHAR_VALUE или SHORT_VALUE, результат по умолчанию всегда положительный (т.е. соответствует uchar и ushort, которые целиком "умещаются" в int). В этих случаях, если числа действительно типов uchar и ushort, предупреждения компилятора чисто номинальные, так как мы заведомо уверены в том, что внутри значения типа int заполнены лишь 1 или 2 младших байта, и у них нет знака. Это происходит без искажений.
Однако в случае хранения в файле значений со знаками (типов char и short) приведение становится необходимым, потому что без него отрицательные величины превратятся в "обратные" положительные с тем же битовым представлением (см. врезку "Знаковые и беззнаковые целые" в разделе Арифметические преобразования типов).
Но в любом случае от предупреждений лучше избавиться путем явного приведения типа.
Функция поддерживает режим чтения целого числа размером 3 байта. Его использование не рекомендуется.
Файловый указатель перемещается на количество прочитанных байтов (а не на размер int).
string FileReadString(int handle, int size = -1)
Функция читает из файла с дескриптором handle строку указанного размера в символах. Параметр size обязательно должен быть задан при работе с бинарным файлом (значение по умолчанию подходит только для текстовых файлов, в которых используются символы-разделители). В противном случае строка не читается (функция возвращает пустую строку), а внутренний код ошибки _LastError равен 5016 (FILE_BINSTRINGSIZE).
Таким образом, еще на стадии записи строки в бинарный файл необходимо подумать, каким образом строка будет читаться. Существует 3 основных варианта:
- записывать строки с нулевым терминальным символом на конце; в этом случае их придется анализировать посимвольно в цикле и объединять символы в строку, пока не встретится 0;
- всегда записывать строку фиксированной (предопределенной) длины; длину следует выбрать с запасом для большинства сценариев или по спецификации (техзадания, протокола, и т.д.), но это неэкономно и не дает 100% гарантии, что какая-нибудь редкая строка не окажется укороченной при записи в файл;
- записывать перед строкой её длину как целое число.
Функция FileReadString может работать и с текстовыми файлами. Такое её применение описано в следующем разделе.
Также обратите внимание, что если параметр size равен 0 (что может получиться в ходе каких-либо вычислений), то функция не производит чтение: указатель файла остается на прежнем месте и функция возвращает пустую строку.
В качестве примера для данного раздела мы усовершенствуем скрипт FileStruct.mq5 из предыдущего раздела. Новое название программы FileAtomic.mq5.
Задача останется прежней: сохранить в двоичный файл заданное количество урезанных структур MqlRates с котировками. Но теперь структура FileHeader станет классом (причем сигнатура формата будет храниться в строке, а не в массиве символов), заголовок этого типа и массив котировок войдут в состав другого управляющего класса Candles, и оба класса будут унаследованы от интерфейса Persistent для записи произвольных объектов в файл и чтения из файла.
А вот и сам интерфейс:
interface Persistent
|
Для большего приближения к реальности предусмотрим в классе FileHeader сохранение и проверку при чтении не только сигнатуры формата (поменяем её на "CANDLES/1.1"), но и названия текущего инструмента и таймфрейма графика (подробнее про _Symbol и _Period).
Запись производится в реализации метода write, унаследованного от интерфейса.
class FileHeader : public Persistent
|
Сигнатуру мы записываем точно по её длине, поскольку образец хранится в объекте и при чтении будет задана та же длина.
Для инструмента текущего графика мы предварительно сохраняем в файле длину его имени (достаточно 1 байта для длин вплоть до 255), и только после неё — саму строку.
Название таймфрейма всегда не превышает 3 символа, если из него исключить постоянный префикс "PERIOD_", поэтому для этой строки выбрана фиксированная длина. Получение имени таймфрейма без префикса поручено вспомогательной функции PeriodToString: она находится в отдельном заголовочном файле Periods.mqh (он будет более подробно рассмотрен в разделе Символы и таймфреймы).
Чтение выполняется в методе read в обратном порядке (разумеется, подразумевается, что чтение будет выполняться в другой, новый объект).
bool read(int handle) override
|
Если какое-либо из свойств (сигнатура, символ, таймфрейм) не совпадает в файле и на текущем графике, функция возвращает признак ошибки: false.
Обратное преобразование имени таймфрейма в перечисление ENUM_TIMEFRAMES делает функция StringToPeriod, также из файла Periods.mqh.
Основной класс для запроса, сохранения и чтения архива котировок Candles выглядит следующим образом.
class Candles : public Persistent
|
Полями являются заголовок типа FileHeader, запрошенное количество баров limit и массив, принимающий структуры MqlRates от MetaTrader 5. Заполнение массива производится в конструкторе. В случае ошибки поле limit сбрасывается в ноль.
Будучи производным от интерфейса Persistent, класс Candles требует реализации методов write и read. В методе write мы сначала поручаем объекту заголовка сохранить себя, а затем дописываем в файл количество котировок, диапазон дат (для справки) и сам массив.
bool write(int handle) override
|
Чтение производится в обратном порядке:
bool read(int handle) override
|
В реальной программе архивирования котировок наличие диапазона дат позволило бы по заголовкам файлов выстроить их правильную последовательность на продолжительной истории и в некоторой степени защитило бы от произвольного переименования файлов.
Для контроля процесса имеется простой метод print:
void print() const
|
В главной функции скрипта мы создаем два объекта Candles, и с помощью одного сначала сохраняем архив котировок, а с помощью другого затем восстанавливаем. Файлы управляются уже известной нам оберткой FileHandle (см. раздел Управление дескрипторами файлов).
const string filename = "MQL5Book/atomic.raw";
|
Вот пример вывода в журнал исходных данных для XAUUSD,H1:
FileOpen(filename,FILE_BIN|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ)=1 / ok CopyRates(_Symbol,_Period,0,limit,rates)=10 / ok FileWriteString(handle,signature,StringLen(signature))=11 / ok FileWriteInteger(handle,StringLen(_Symbol),CHAR_VALUE)=1 / ok FileWriteString(handle,_Symbol)=6 / ok FileWriteString(handle,PeriodToString(),3)=3 / ok FileWriteInteger(handle,limit)=4 / ok FileWriteLong(handle,rates[0].time)=8 / ok FileWriteLong(handle,rates[limit-1].time)=8 / ok [time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume] [0] 2021.08.17 15:00:00 1791.40 1794.57 1788.04 1789.46 8157 5 0 [1] 2021.08.17 16:00:00 1789.46 1792.99 1786.69 1789.69 9285 5 0 [2] 2021.08.17 17:00:00 1789.76 1790.45 1780.95 1783.30 8165 5 0 [3] 2021.08.17 18:00:00 1783.30 1783.98 1780.53 1782.73 5114 5 0 [4] 2021.08.17 19:00:00 1782.69 1784.16 1782.09 1782.49 3586 6 0 [5] 2021.08.17 20:00:00 1782.49 1786.23 1782.17 1784.23 3515 5 0 [6] 2021.08.17 21:00:00 1784.20 1784.85 1782.73 1783.12 2627 6 0 [7] 2021.08.17 22:00:00 1783.10 1785.52 1782.37 1785.16 2114 5 0 [8] 2021.08.17 23:00:00 1785.11 1785.84 1784.71 1785.80 922 5 0 [9] 2021.08.18 01:00:00 1786.30 1786.34 1786.18 1786.20 13 5 0 |
А вот пример восстановленных данных (напомним, что структуры сохраняются в урезанном виде по нашему гипотетическому техзаданию):
FileOpen(filename,FILE_BIN|FILE_READ|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE)=2 / ok FileReadString(handle,StringLen(signature))=CANDLES/1.1 / ok FileReadInteger(handle,CHAR_VALUE)=6 / ok FileReadString(handle,len)=XAUUSD / ok FileReadString(handle,3)=H1 / ok FileReadInteger(handle)=10 / ok FileReadLong(handle)=1629212400 / ok FileReadLong(handle)=1629248400 / ok [time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume] [0] 2021.08.17 15:00:00 1791.40 1794.57 1788.04 1789.46 0 0 0 [1] 2021.08.17 16:00:00 1789.46 1792.99 1786.69 1789.69 0 0 0 [2] 2021.08.17 17:00:00 1789.76 1790.45 1780.95 1783.30 0 0 0 [3] 2021.08.17 18:00:00 1783.30 1783.98 1780.53 1782.73 0 0 0 [4] 2021.08.17 19:00:00 1782.69 1784.16 1782.09 1782.49 0 0 0 [5] 2021.08.17 20:00:00 1782.49 1786.23 1782.17 1784.23 0 0 0 [6] 2021.08.17 21:00:00 1784.20 1784.85 1782.73 1783.12 0 0 0 [7] 2021.08.17 22:00:00 1783.10 1785.52 1782.37 1785.16 0 0 0 [8] 2021.08.17 23:00:00 1785.11 1785.84 1784.71 1785.80 0 0 0 [9] 2021.08.18 01:00:00 1786.30 1786.34 1786.18 1786.20 0 0 0 |
Легко убедиться, что данные сохранены и прочитаны правильно. А теперь посмотрим, как они выглядят внутри файла:
Просмотр внутреннего устройства бинарного файла с архивом котировок во внешней программе
Здесь цветом подсвечены различные поля нашего заголовка: сигнатура, длина названия символа, название символа, название таймфейма и т.д.