Открытие и закрытие файлов

Для записи и чтения данных из файла большинство функций MQL5 требуют предварительно этот файл открыть. Для этой цели предназначена функция FileOpen. После выполнения требуемых операций открытый файл следует закрыть с помощью функции FileClose. Дело в том, что открытый файл может, в зависимости от примененных опций, быть заблокированным для доступа из других программ. Кроме того, операции с файлами буферизуются в памяти (кэше) из соображений повышения быстродействия, и без закрытия файла новые данные в него могут физически не выгружаться на протяжении какого-то времени. Это особенно критично, если записываемые данные ждет внешняя программа (например, при интеграции MQL-программы с другими системами). Про альтернативный способ "сброса" буфера на диск мы узнаем из описания функции FileFlush.

С открытым файлом в MQL-программе связывается специальное целое число — дескриптор. Его возвращает функция FileOpen. Все операции, связанные с доступом или модификацией внутреннего содержимого файла, требуют указания этого идентификатора в соответствующих функциях API. Те функции, которые оперируют файлом целиком (копируют, удаляют, перемещают, проверяют существование), не требуют дескриптора. Для выполнения этих действий открывать файл не надо.

int FileOpen(const string filename, int flags, const short delimiter = '\t', uint codepage = CP_ACP)

int FileOpen(const string filename, int flags, const string delimiter, uint codepage = CP_ACP)

Функция открывает файл с указанным именем и в режиме, заданном параметром flags. Параметр filename может содержать вложенные папки перед непосредственным именем файла. В этом случае, если файл открывается на запись и требуемая иерархия папок еще не существует, она будет создана.

Параметр flags должен содержать комбинацию констант, описывающих требуемый режим работы с файлом. Комбинация выполняется с помощью операций побитового ИЛИ. Ниже приведена таблица доступных констант.

Идентификатор

Значение

Описание

FILE_READ

1

Файл открывается для чтения

FILE_WRITE

2

Файл открывается для записи

FILE_BIN

4

Двоичный режим чтения-записи, без преобразования данных из строки и в строку

FILE_CSV

8

Файл типа CSV; записываемые данные преобразуются в текст соответствующего типа (Unicode или ANSI, см. далее), а при чтении производится обратная конвертация из текста в требуемый тип (указывается в функции чтения); одна CSV-запись — это отдельная строка текста, ограниченная символами перевода строки (обычно CRLF); внутри CSV-записи элементы разделяются символом-разделителем (параметр delimiter);

FILE_TXT

16

Простой текстовый файл, аналогичный режиму CSV, но символ-разделитель не используется (значение параметра delimiter игнорируется)

FILE_ANSI

32

Строки типа ANSI (однобайтовые символы)

FILE_UNICODE

64

Строки типа Unicode (двухбайтовые символы)

FILE_SHARE_READ

128

Совместный доступ по чтению со стороны нескольких программ

FILE_SHARE_WRITE

256

Совместный доступ по записи со стороны нескольких программ

FILE_REWRITE

512

Разрешение перезаписать файл (если он уже существует) в функциях FileCopy и FileMove

FILE_COMMON

4096

Расположение файла в общей папке всех клиентских терминалов /Terminal/Common/Files (флаг используется при открытии файлов (FileOpen), копировании файлов (FileCopy, FileMove) и проверке существования файлов (FileIsExist))

При открытии файла обязательно должен быть указан один из флагов FILE_WRITE, FILE_READ или их комбинация.

Флаги FILE_SHARE_READ и FILE_SHARE_WRITE не заменяют и не отменяют необходимости указывать флаги FILE_READ и FILE_WRITE.

Среда исполнения MQL-программ всегда буферизует файлы на чтение, что эквивалентно неявному добавлению флага FILE_READ. В связи с этим для нормальной работы с разделяемыми файлами всегда следует использовать FILE_SHARE_READ (даже если известно, что другой процесс открывает файл только на запись).

Если не указан ни один из флагов FILE_CSV, FILE_BIN, FILE_TXT, подразумевается FILE_CSV, как наиболее приоритетный. Если указано несколько из этих трех флагов, применяется наиболее приоритетный из переданных (выше они перечислены в порядке убывания приоритета).

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

Параметр delimiter, влияющий только на CSV, может быть типа ushort или string. Во втором случае, если длина строки больше 1, будет использован только первый её символ.

Параметр codepage влияет только на файлы, открываемые в текстовом режиме (FILE_TXT или FILE_CSV), причем только в том случае, если для строк выбран режим FILE_ANSI. Если строки хранятся в Unicode (FILE_UNICODE), кодовая страница не важна.

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

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

Все настройки режима работы, сделанные в момент открытия файла, сохраняются без изменений все время, пока файл открыт. Если возникнет необходимость изменить режим, файл следует закрыть и открыть вновь с новыми параметрами.

Для каждого открытого файла среда выполнения MQL-программ поддерживает внутренний указатель, то есть текущую позицию внутри файла. Сразу после открытия файла указатель установлен на начало (позиция равна 0). В процессе записи или чтения позиция соответствующим образом сдвигается, согласно передаваемому или получаемому объему данных из различных файловых функций. Также существует возможность напрямую влиять на позицию (перемещать назад или вперед). Все эти возможности будут рассмотрены в последующих разделах.

FILE_READ и FILE_WRITE в разных сочетаниях позволяют добиться нескольких сценариев:

  • FILE_READ — открытие файла, только если он существует; в противном случае функция вернет ошибку, а новый файл не создается;
  • FILE_WRITE — создание нового файла, если его еще не было, или открытие существующего файла, причем его содержимое очищается, а размер обнуляется;
  • FILE_READ|FILE_WRITE — открытие существующего файла вместе со всем его содержимым или создание нового файл, если его еще не было.

Как видно, некоторые сценарии недоступны только за счет флагов. В частности, открыть файл на запись, только если он уже существует, — нельзя. Этого можно добиться, используя дополнительные функции, например, FileIsExist. Также не получится "автоматом" обнулить файл, открываемый для комбинации чтения и записи: в этом случае MQL5 всегда оставляет содержимое.

Чтобы дописывать данные в файл нужно не только открыть файл в режиме FILE_READ|FILE_WRITE, но и переместить текущую позицию внутри файла в его конец с помощью вызова FileSeek.

Правильное описание разделяемого доступа к файлу — обязательное условие успешности выполнения FileOpen. Управляется данный аспект следующим образом.

  • Если не указан ни один из флагов FILE_SHARE_READ и FILE_SHARE_WRITE, то текущая программа получает исключительный доступ к файлу, если открывает его первой. Если этот же файл уже был открыт кем-то раньше (другой или этой же программой), вызов функции завершится ошибкой.
  • При установке флага FILE_SHARE_READ, программа разрешает последующие запросы на открытие того же файла на чтение. Если в момент вызова функции файл уже открыт другой или этой же программой на чтение, а данный флаг не установлен, функция завершится ошибкой.
  • При установке флага FILE_SHARE_WRITE, программа разрешает последующие запросы на открытие того же файла на запись. Если в момент вызова функции файл уже открыт другой или этой же программой на запись, а данный флаг не установлен, функция завершится ошибкой.

Разделение доступа проверяется не только в отношении других MQL-программ или внешних по отношению к MetaTrader 5 процессов, но и к той же самой MQL-программе, если она открывает файл повторно.

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

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

void FileClose(int handle)

Функция закрывает ранее открытый файл по его дескриптору.

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

При завершении программы открытые файлы принудительно закрываются, а буфер записи, если он не пустой, записывается на диск. Вместе с тем рекомендуется закрывать файлы явно.

Закрытие файла по завершении работы с ним — важное правило, которому необходимо следовать. Это связано не только с кэшированием записываемой информации, которая может какое-то время оставаться в оперативной памяти и не сохраненной на диск (о чем уже упоминалось выше), если не закрыть файл. Помимо этого открытый файл потребляет некий внутренний ресурс операционной системы, и речь здесь — не о месте на диске. Количество одновременно открытых файлов ограничено (может быть несколько сотен или тысяч в зависимости от настроек Windows). Если множество программ будет держать открытыми большое количество файлов, этот лимит может быть исчерпан, и попытки открыть новые файлы завершатся ошибками.

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

Мы создадим класс-обертку, после того как проверим работу функций FileOpen и FileClose в чистом виде.

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

PRT(FileLoad(filenameread));

Здесь результат вызова FileLoad отправляется в журнал, но получить его в вызывающей строке кода невозможно. Правда, нам это было и не нужно. Но сейчас функция FileOpen будет возвращать дескриптор файла, и его следует сохранить в переменной для дальнейших манипуляций с файлом.

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

Для решения проблем нам потребуется вспомогательная функция печати, возвращающая печатаемое значение. А её вызов мы "упакуем" в новый макрос PRTF:

#include <MQL5Book/MqlError.mqh>
  
#define PRTF(AResultPrint(#A, (A))
  
template<typename T>
T ResultPrint(const string sconst T retval = 0)
{
   const string err = E2S(_LastError) + "(" + (string)_LastError + ")";
   Print(s"="retval" / ", (_LastError == 0 ? "ok" : err));
   ResetLastError(); // очистка флага ошибки для следующего вызова
   return retval;
}

С помощью магического оператора преобразования в строку '#' мы получаем подробный описатель фрагмента кода (выражения A), который передается первым аргументом в ResultPrint. Само выражение (аргумент макроса) вычисляется (если там есть функция, она вызывается) и его результат передается вторым аргументом в ResultPrint. Далее уже в дело вступает привычная функция Print, а в заключение тот же результат возвращается в вызывающий код.

Для того чтобы не заглядывать в справку за расшифровкой кодов ошибок был подготовлен макрос E2S, который использует перечисление MQL_ERROR со всеми ошибками MQL5. Его можно найти в заголовочном файле MQL5/Include/MQL5Book/MqlError.mqh. Сам новый макрос и функция ResultPrint определены в файле PRTF.mqh, рядом с тестовыми скриптами.

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

Один из файлов — MQL5Book/rawdata — должен уже существовать, поскольку был создан скриптом из раздела Запись и чтение файлов в упрощенном режиме. Другой файл создадим в ходе теста.

Тип файлов выберем FILE_BIN, но это не принципиально: с FILE_TXT или FILE_CSV работа на данном этапе ведется аналогично.

Под дескрипторы файлов зарезервируем массив, чтобы в конце скрипта закрыть все файлы сразу.

Первым откроем MQL5Book/rawdata в режиме чтения без разделения доступа. Предполагая, что файл не занят никаким сторонним приложением, можем ожидать успешное получение дескриптора.

void OnStart()
{
   int ha[4] = {}; // массив под дескрипторы тестовых файлов
   
   // этот файл должен существовать после запуска FileSaveLoad.mq5
   const string rawdata = "MQL5Book/rawdata";
   ha[0] = PRTF(FileOpen(rawdataFILE_BIN FILE_READ)); // 1 / ok

Если попытаемся открыть тот же файл еще раз, столкнемся с ошибкой, потому что ни первый, ни второй вызов не разрешает совместный доступ.

 ha[1] = PRTF(FileOpen(rawdataFILE_BIN FILE_READ)); // -1 / CANNOT_OPEN_FILE(5004)

Закроем первый дескриптор, откроем файл заново, но уже с правами на совместное чтение, и убедимся, что повторное открытие теперь работает (правда в нем также требуется разрешить разделяемое чтение):

   FileClose(ha[0]);
   ha[0] = PRTF(FileOpen(rawdataFILE_BIN FILE_READ FILE_SHARE_READ)); // 1 / ok
   ha[1] = PRTF(FileOpen(rawdataFILE_BIN FILE_READ FILE_SHARE_READ)); // 2 / ok

Открыть файл на запись (FILE_WRITE) не получится, поскольку два предыдущих вызова FileOpen разрешают только FILE_SHARE_READ.

   ha[2] = PRTF(FileOpen(rawdataFILE_BIN FILE_READ FILE_WRITE FILE_SHARE_READ));
   // -1 / CANNOT_OPEN_FILE(5004)

Теперь попробуем создать новый файл MQL5Book/newdata. Если открывать его только для чтения, файл не создастся.

   const string newdata = "MQL5Book/newdata";
   ha[3] = PRTF(FileOpen(newdataFILE_BIN FILE_READ));
   // -1 / CANNOT_OPEN_FILE(5004)

Для создания файла необходимо указать режим FILE_WRITE (наличие FILE_READ здесь некритично, но делает вызов более универсальным: как мы помним, в таком сочетании инструкция гарантирует, что откроется либо старый файл, если он есть, либо создастся новый).

   ha[3] = PRTF(FileOpen(newdataFILE_BIN FILE_READ FILE_WRITE)); // 3 / ok

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

   long x[1] = {0x123456789ABCDEF0};
   PRTF(FileSave(newdatax)); // false

Этот вызов заканчивается неудачей, потому что дескриптор был открыт без прав совместного доступа. Закроем и вновь откроем файл с максимальными "разрешениями".

   FileClose(ha[3]);
   ha[3] = PRTF(FileOpen(newdata,
      FILE_BIN FILE_READ FILE_WRITE FILE_SHARE_READ FILE_SHARE_WRITE)); // 3 / ok

На этот раз FileSave работает, как ожидалось.

   PRTF(FileSave(newdatax)); // true

Вы можете заглянуть в папку MQL5/Files/MQL5Book/ и найти там файл newdata длиной 8 байтов.

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

Для аккуратного завершения работы закроем явным образом все открытые файлы.

   for(int i = 0i < ArraySize(ha); ++i)
   {
      if(ha[i] != INVALID_HANDLE)
      {
        FileClose(ha[i]);
      }
   }
}