Управление дескрипторами файлов

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

Данный принцип хорошо известен в программировании и называется инициализацией с захватом ресурса (RAII, Resource Acquisition Is Initialization). Использование RAII упрощает контроль за ресурсами и обеспечивает их корректное состояние. В частности, это особенно эффективно, если выход из функции, где открывается файл (и для него создается объект-владелец), производится из нескольких разных мест.

Область применения RAII не ограничивается файлами. В разделе Шаблоны объектных типов мы создали класс AutoPtr, который управляет указателем на объект и является другим примером данной концепции. Ведь указатель — это тоже ресурс (память), и его очень просто "потерять" и накладно освобождать в нескольких разных ветках алгоритма.  

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

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

class FileHandle
{
   int handle;
public:
   FileHandle(const int h = INVALID_HANDLE) : handle(h)
   {
   }
   
   FileHandle(int &holderconst int h) : handle(h)
   {
      holder = h;
   }
   
   int operator=(const int h)
   {
      handle = h;
      return h;
   }
   ...

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

Но можно обойтись и без алиаса. Для этих случаев в классе определен оператор '~', возвращающий значение внутренней переменной handle.

   int operator~() const
   {
      return handle;
   }

Наконец, самое главное, ради чего класс и был задуман, — "умный" деструктор:

   ~FileHandle()
   {
      if(handle != INVALID_HANDLE)
      {
         ResetLastError();
         // установит код внутренней ошибки, если handle недействителен
         FileGetInteger(handleFILE_SIZE);
         if(_LastError == 0)
         {
            #ifdef FILE_DEBUG_PRINT
               Print(__FUNCTION__": Automatic close for handle: "handle);
            #endif
            FileClose(handle);
         }
         else
         {
            PrintFormat("%s: handle %d is incorrect, %s(%d)",
               __FUNCTION__handleE2S(_LastError), _LastError);
         }
      }
   }

В нем, после нескольких проверок, вызывается FileClose для контролируемой переменной handle. Дело в том, что файл может быть закрыт явным образом в другом месте программы, хотя это больше и не требуется при наличии данного класса. В результате, дескриптор может стать недействительным к моменту вызова деструктора, когда исполнение алгоритма покинет блок, в котором определен объект FileHandle. Для выяснения этого обстоятельства используется фиктивный вызов функции FileGetInteger: фиктивный, потому что он ничего полезного не делает. Если после вызова код внутренней ошибки остался равным 0, дескриптор рабочий.

В принципе, всех этих проверок можно не делать и написать просто:

   ~FileHandle()
   {
      if(handle != INVALID_HANDLE)
      {
         FileClose(handle);
      }
   }

Если дескриптор испорчен, FileClose не станет "возмущаться". Но мы добавили проверки, чтобы иметь возможность вывести диагностическую информацию.

Попробуем класс FileHandle в действии. Тестовый скрипт для него называется FileHandle.mq5.

const string dummy = "MQL5Book/dummy";
   
void OnStart()
{
   // создаем новый файл или открываем существующий и обнуляем его
   FileHandle fh1(PRTF(FileOpen(dummy,
      FILE_TXT FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ))); // 1
   // другой вариант подключения дескриптора через '='
   int h = PRTF(FileOpen(dummy,
      FILE_TXT FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ)); // 2
   FileHandle fh2 = h;
   // и еще один поддерживаемый синтаксис:
   // int f;
   // FileHandle ff(f, FileOpen(dummy,
   //    FILE_TXT | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
   
   // здесь предполагается запись данных 
   // ...
   
   // закрываем файл вручную (это не обязательно: сделано, чтобы продемонстрировать,
   // что FileHandle это обнаружит и не станет пытаться закрыть повторно)
   FileClose(~fh1); // оператор '~', примененный к объекту, возвращает дескриптор
   
   // дескриптор в переменной 'h', привязанный к объекту 'fh2', не закрыт вручную
   // и будет автоматически закрыт в деструкторе
}

Согласно выводу в журнал, всё действует по плану:

   FileHandle::~FileHandle: Automatic close for handle: 2
   FileHandle::~FileHandle: handle 1 is incorrect, INVALID_FILEHANDLE(5007)

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

Такой класс реализован в файле FileHolder.mqh и демонстрируется в скрипте FileHolder.mq5. Один экземпляр FileHolder сам создает по запросу вспомогательные объекты-наблюдатели класса FileOpener, который имеет общие черты с FileHandle, в особенности, деструктор, а также поле handle.

Для открытия файла через FileHolder следует использовать его метод FileOpen (его сигнатура повторяет сигнатуру стандартной функции FileOpen).

class FileHolder
{
   static FileOpener *files[];
   int expand()
   {
      return ArrayResize(filesArraySize(files) + 1) - 1;
   }
public:
   int FileOpen(const string filenameconst int flags,
                const ushort delimiter = '\t', const uint codepage = CP_ACP)
   {
      const int n = expand();
      if(n > -1)
      {
         files[n] = new FileOpener(filenameflagsdelimitercodepage);
         return files[n].handle;
      }
      return INVALID_HANDLE;
   }

Все объекты FileOpener складываются в массиве files для отслеживания их времени жизни. Там же нулевыми элементами отмечаются моменты регистрации локальных контекстов (блоков кода), в которых создаются объекты FileHolder — за это отвечает конструктор FileHolder.

   FileHolder()
   {
      const int n = expand();
      if(n > -1)
      {
         files[n] = NULL;
      }
   }

Как мы знаем, в процессе выполнения программы, она заходит во вложенные блоки кода (вызывает функции). Если в них требуется управление локальными дескрипторами файлов, там следует описать объекты FileHolder (по одному на блок или реже). Все такие описания по правилам стека (первым пришел, последним ушел) складываются в files и затем освобождаются в обратном порядке по мере того, как программа покидает контексты. В каждый такой момент вызывается деструктор.

   ~FileHolder()
   {
      for(int i = ArraySize(files) - 1i >= 0; --i)
      {
         if(files[i] == NULL)
         {
            // уменьшаем массив и выходим
            ArrayResize(filesi);
            return;
         }
         
         delete files[i];
      }
   }

Его задача — удалить последние объекты FileOpener в массиве вплоть до первого встреченного нулевого элемента, который означает границу контекста (далее в массиве идут дескрипторы из другого, внешнего контекста).

В полном объеме данный класс оставлен для самостоятельного изучения.

Посмотрим на его применение в тестовом скрипте FileHolder.mq5. Помимо функции OnStart здесь имеется функция SubFunc, и в обоих контекстах ведется работа с файлами.

const string dummy = "MQL5Book/dummy";
   
void SubFunc()
{
   Print(__FUNCTION__" enter");
   FileHolder holder;
   int h = PRTF(holder.FileOpen(dummy,
      FILE_BIN FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ));
   int f = PRTF(holder.FileOpen(dummy,
      FILE_BIN FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ));
   // используем h и f
   // ...
   // не нужно закрывать файлы вручную и отслеживать ранние выходы из функции
   Print(__FUNCTION__" exit");
}
 
void OnStart()
{
   Print(__FUNCTION__" enter");
   
   FileHolder holder;
   int h = PRTF(holder.FileOpen(dummy,
      FILE_BIN FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ));
   // запись данных и другие действия над файлом по дескриптору
   // ...
   /*
   int a[] = {1, 2, 3};
   FileWriteArray(h, a);
   */
   
   SubFunc();
   SubFunc();
   
   if(rand() > 32000// имитируем ветвление по условиям
   {
      // благодаря holder нам не нужен явный вызов
      // FileClose(h);
      Print(__FUNCTION__" return");
      return// может быть много выходов из функции
   }
   
   /*
      ... еще код
   */
   
   // благодаря holder нам не нужен явный вызов
   // FileClose(h);
   Print(__FUNCTION__" exit");
}

Мы не закрыли ни один дескриптор вручную, экземпляры FileHolder сделают это автоматически в деструкторах.

Вот пример вывода в журнал:

OnStart enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=1 / ok
SubFunc enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=2 / ok
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=3 / ok
SubFunc exit
FileOpener::~FileOpener: Automatic close for handle: 3
FileOpener::~FileOpener: Automatic close for handle: 2
SubFunc enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=2 / ok
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=3 / ok
SubFunc exit
FileOpener::~FileOpener: Automatic close for handle: 3
FileOpener::~FileOpener: Automatic close for handle: 2
OnStart exit
FileOpener::~FileOpener: Automatic close for handle: 1