Gestión de descriptores de archivo

Dado que necesitamos recordar constantemente los archivos abiertos y liberar los descriptores locales al salir de las funciones, sería eficiente confiar toda la rutina a objetos especiales.

Este enfoque es bien conocido en programación y se denomina RAII (Resource Acquisition Is Initialization, por sus siglas en inglés). El uso de RAII facilita el control de los recursos y garantiza que se encuentran en el estado correcto. En concreto, esto es especialmente efectivo si la función que abre el archivo (y crea un objeto propietario para él) sale desde varios lugares diferentes.

El ámbito de aplicación de RAII no se limita a los archivos. En la sección Plantillas de tipos de objeto hemos creado la clase AutoPtr, que administra un puntero a un objeto. Este era otro ejemplo de este concepto, ya que un puntero es también un recurso (memoria) y es muy fácil perderlo, además de que consume muchos recursos liberarlo en varias ramas diferentes del algoritmo.

Una clase envolvente de archivos también puede ser útil de otra manera. La API de archivos no proporciona una función que permita obtener el nombre de un archivo mediante un descriptor (a pesar de que esa relación existe sin duda internamente). Al mismo tiempo, dentro del objeto, podemos almacenar este nombre e implementar nuestro propio enlace al descriptor.

En el caso más simple necesitamos alguna clase que almacene un descriptor de archivo y lo cierre automáticamente en el destructor. En el archivo FileHandle.mqh se muestra un ejemplo de aplicación.

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;
   }
   ...

Dos constructores, así como un operador de asignación sobrecargado, garantizan que un objeto esté vinculado a un archivo (descriptor). El segundo constructor permite pasar una referencia a una variable local (del código de llamada), que además obtendrá un nuevo descriptor. Será una especie de alias externo para el mismo descriptor, que podrá utilizarse de la forma habitual en otras llamadas a funciones.

Pero también se puede prescindir de un alias. Para estos casos, la clase define el operador '~', que devuelve el valor de la variable interna handle.

   int operator~() const
   {
      return handle;
   }

Por último, lo más importante para lo que se implementó la clase es el destructor inteligente:

   ~FileHandle()
   {
      if(handle != INVALID_HANDLE)
      {
         ResetLastError();
         // will set internal error code if handle is invalid
         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);
         }
      }
   }

En él, tras varias comprobaciones, se llama a FileClose para la variable controlada handle. La cuestión es que el archivo puede cerrarse explícitamente en otra parte del programa, aunque esto ya no es necesario con esta clase. Como resultado, el descriptor puede dejar de ser válido en el momento en que se llama al destructor cuando la ejecución del algoritmo abandona el bloque en el que está definido el objeto FileHandle. Para averiguarlo, se realiza una llamada ficticia a la función FileGetInteger. Es ficticia porque no hace nada útil. Si el código de error interno sigue siendo 0 después de la llamada, el descriptor es válido.

Podemos omitir todas estas comprobaciones y escribir simplemente lo siguiente:

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

Si el descriptor está dañado, FileClose no devolverá ninguna advertencia. No obstante, hemos añadido comprobaciones para poder emitir información de diagnóstico.

Vamos a probar la clase FileHandle en acción. El script de prueba se llama FileHandle.mq5.

const string dummy = "MQL5Book/dummy";
   
void OnStart()
{
   // creating a new file or open an existing one and reset it
   FileHandle fh1(PRTF(FileOpen(dummy
      FILE_TXT FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ))); // 1
   // another way to connect the descriptor via '='
   int h = PRTF(FileOpen(dummy
      FILE_TXT FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ)); // 2
   FileHandle fh2 = h;
   // and another supported syntax:
   // int f;
   // FileHandle ff(f, FileOpen(dummy,
   //    FILE_TXT | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
   
   // data is supposed to be written here
   // ...
   
   // close the file manually (this is not necessary; only done to demonstrate 
   // that the FileHandle will detect this and won't try to close it again)
   FileClose(~fh1); // operator '~' applied to an object returns a handle
   
   // descriptor handle in variable 'h' bound to object 'fh2' is not manually closed
   // and will be automatically closed in the destructor
}

Según la salida en el registro, todo funciona según lo previsto:

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

Sin embargo, si hay muchos archivos, crear una copia del objeto de seguimiento para cada uno de ellos puede convertirse en un inconveniente. Para estas situaciones tiene sentido diseñar un único objeto que recoja todos los descriptores en un contexto determinado (por ejemplo, dentro de una función).

Dicha clase se implementa en el archivo FileHolder.mqh y se muestra en el script FileHolder.mq5. Una copia del propio FileHolder crea a petición objetos observadores auxiliares de la clase FileOpener, que comparte características comunes con FileHandle, especialmente el destructor, así como el campo handle.

Para abrir un archivo a través de FileHolder debe utilizar su método FileOpen (su firma repite la firma de la función estándar 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;
   }

Todos los objetos de FileOpener se suman en el array files para hacer un seguimiento de su vida útil. En el mismo lugar, los elementos cero marcan los momentos de registro de los contextos locales (bloques de código) en los que se crean los objetos FileHolder. El constructor FileHolder se encarga de ello.

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

Como sabemos, durante la ejecución de un programa, éste entra en bloques de código anidados (llama a funciones). Si requieren la administración de descriptores de archivo locales, los objetos FileHolder (uno por bloque o menos) deben describirse allí. Según las reglas de la pila (primero en entrar, último en salir), todas esas descripciones se suman en files y luego se liberan en orden inverso a medida que el programa abandona los contextos. El destructor es llamado en cada uno de estos momentos.

   ~FileHolder()
   {
      for(int i = ArraySize(files) - 1i >= 0; --i)
      {
         if(files[i] == NULL)
         {
            // decrement array and exit
            ArrayResize(filesi);
            return;
         }
         
         delete files[i];
      }
   }

Su tarea consiste en eliminar los últimos objetos FileOpener del array hasta el primer elemento cero encontrado, lo cual indica el límite del contexto (más adelante en el array hay descriptores de otro contexto externo).

Puede estudiar toda la clase por su cuenta.

Veamos su uso en el script de prueba FileHolder.mq5. Además de la función OnStart, dispone de SubFunc. Las operaciones con archivos se realizan en ambos contextos.

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));
   // use h and f
   // ...
   // no need to manually close files and track early function exits
   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));
   // writing data and other actions on the file by descriptor
   // ...
   /*
   int a[] = {1, 2, 3};
   FileWriteArray(h, a);
   */
   
   SubFunc();
   SubFunc();
   
 if(rand() >32000// simulate branching by conditions
   {
      // thanks to the holder we don't need an explicit call
      // FileClose(h);
      Print(__FUNCTION__" return");
      return// there can be many exits from the function
   }
   
   /*
     ... more code
   */
   
   // thanks to the holder we don't need an explicit call
   // FileClose(h);
   Print(__FUNCTION__" exit");
}

No hemos cerrado ningún manejador manualmente, las instancias de FileHolder lo harán automáticamente en los destructores.

He aquí un ejemplo de salida de registro:

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

Esta página utiliza cookies. Aprenda más sobre ellas en nuestra Política sobre Cookies.