Escritura y lectura de arrays

Dos funciones MQL5 sirven para escribir y leer arrays: FileWriteArray y FileReadArray. Con los archivos binarios, permiten manejar arrays de cualquier tipo integrado que no sean cadenas, así como arrays de estructuras simples que no contengan campos de cadena, objetos, punteros y arrays dinámicos. Estas limitaciones están relacionadas con la optimización de los procesos de escritura y lectura, lo cual es posible gracias a la exclusión de tipos con longitudes variables. Las cadenas, los objetos y los arrays dinámicos son exactamente así.

Al mismo tiempo, cuando se trabaja con archivos de texto, estas funciones son capaces de operar sobre arrays del tipo string (otros tipos de arrays en archivos con modo FILE_TXT/FILE_CSV no están permitidos por estas funciones). Estos arrays se almacenan en un archivo con el siguiente formato: un elemento por línea.

Si necesita almacenar estructuras o clases sin restricciones de tipo en un archivo, utilice funciones específicas de tipo que procesen un valor por llamada. Se describen en dos secciones sobre escritura y lectura de variables de tipos integrados: para archivos binarios y de texto.

Además, el soporte para estructuras con cadenas puede organizarse mediante la optimización interna del almacenamiento de información. Por ejemplo, en lugar de campos de cadena, puede utilizar campos de número entero, que contendrán los índices de las cadenas correspondientes en un array independiente con cadenas. Dada la posibilidad de redefinir muchas operaciones (en particular, la asignación) utilizando herramientas de programación orientada a objetos y de obtener un elemento estructural de un array por número, el aspecto del algoritmo prácticamente no cambiará. Pero al escribir, puede abrir primero un archivo en modo binario y llamar a FileWriteArray para obtener un array con un tipo de estructura simplificada y, a continuación, volver a abrir el archivo en modo texto y añadirle un array con todas las cadenas usando la segunda llamada a FileWriteArray. Para leer un archivo de este tipo, debe proporcionar un encabezado al principio del mismo que contenga el número de elementos de los arrays para poder pasarlo como el parámetro count a FileReadArray (véase más adelante).

Si necesita guardar o leer, no un array de estructuras, sino una única estructura, utilice las funciones FileWriteStruct y FileReadStruct, que se describen en la sección siguiente.

Vamos a estudiar las firmas de las funciones y, a continuación, consideraremos un ejemplo general (FileArray.mq5).

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

La función escribe array array en un archivo con el descriptor handle. El array puede ser multidimensional. Los parámetros start y count permiten establecer el rango de elementos; por defecto, es igual a todo el array. En el caso de arrays multidimensionales, el índice start y el número de elementos count se refieren a la numeración continua en todas las dimensiones, no a la primera dimensión del array. Por ejemplo, si el array tiene la configuración [][5], entonces el valor start igual a 7 apuntará al elemento con índices [1][2], y count = 2 le añadirá el elemento [1][3].

La función devuelve el número de elementos escritos. En caso de error, será 0.

Si handle se recibe en modo binario, los arrays pueden ser de cualquier tipo integrado excepto cadenas, o tipos de estructura simple. Si handle se abre en cualquiera de los modos de texto, el array debe ser del tipo string.

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

La función lee datos de un archivo con el descriptor handle en un array. El array puede ser multidimensional y dinámico. Para arrays multidimensionales, los parámetros start y count funcionan sobre la base de la numeración continua de elementos en todas las dimensiones, descrita anteriormente. Un array dinámico, si es necesario, aumenta automáticamente de tamaño para ajustarse a los datos que se leen. Si start es mayor que la longitud original del array, estos elementos intermedios contendrán datos aleatorios tras la asignación de memoria (véase el ejemplo).

Tenga en cuenta que la función no puede controlar si la configuración del array utilizado al escribir el archivo coincide con la configuración del array receptor al leer. Básicamente, no hay ninguna garantía de que el archivo que se está leyendo se haya escrito con FileWriteArray.
 
Para comprobar la validez de la estructura de datos se suelen utilizar algunos formatos predefinidos de encabezados iniciales u otros descriptores dentro de los archivos. Las funciones en sí leerán cualquier contenido del archivo dentro de su tamaño y lo colocarán en el array especificado.

Si handle se recibe en modo binario, los arrays pueden ser cualquiera de los tipos integrados que no sean cadenas o tipos de estructura simple. Si handle se abre en modo texto, el array debe ser del tipo string.

Vamos a comprobar el trabajo tanto en modo binario como de texto utilizando el script FileArray.mq5. Para ello, reservaremos dos nombres de archivo.

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

En la función OnStart se describen tres arrays de tipo long y dos arrays de tipo string. Sólo se rellena con datos el primer array de cada tipo, y todos los demás se comprobarán para su lectura una vez que se hayan escrito los archivos.

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

Además, para probar las operaciones con estructuras, se definen los tres tipos siguientes:

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

No podremos utilizar una estructura del tipo TT en las funciones descritas porque contiene campos de cadena. Esto es necesario para demostrar un posible error de compilación en una sentencia comentada (véase más adelante). La herencia entre las estructuras B y XYZ, así como la presencia de un campo cerrado, no son un obstáculo para las funciones FileWriteArray y FileReadArray.

Las estructuras se utilizan para declarar un par de arrays:

 TTtt[]; // empty, because data is not important
   XYZ xyz[1];
   xyz[0].setB(-1);
   xyz[0].x = xyz[0].y = xyz[0].z = clrRed;

Empecemos por el modo binario. Vamos a crear un nuevo archivo o a abrir uno ya existente, volcando su contenido. A continuación, en tres llamadas a FileWriteArray, intentaremos escribir tres arrays: numbers1, text1 y 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);

Los arrays numbers1 y xyz se escriben correctamente, como indica el número de elementos escritos. El array text1 falla con un error FILE_NOTTXT(5012) porque los arrays de cadenas requieren que el archivo se abra en modo texto. Por lo tanto, el contenido xyz se ubicará en el archivo inmediatamente después de todos los elementos de numbers1.

Tenga en cuenta que cada función de escritura (o lectura) comienza escribiendo (o leyendo) datos en la posición actual dentro del archivo, y la desplaza en el tamaño de los datos escritos o leídos. Si este puntero se encuentra al final del archivo antes de la operación de escritura, se incrementa el tamaño del archivo. Si se alcanza el final del archivo durante la lectura, el puntero deja de moverse y el sistema emite un código de error interno especial 5027 (FILE_ENDOFFILE). En un nuevo archivo de tamaño cero, el principio y el final son iguales.

Desde un array text1 se escribieron 0 elementos, por lo que nada en el archivo le recuerda que entre dos llamadas FileWriteArray correctas se produjo un fallo.

En el script de prueba mostramos simplemente el resultado de la función y el estado (código de error) en el registro, pero en un programa real, debemos analizar los problemas sobre la marcha y tomar algunas medidas: arreglar algo en los parámetros, en la configuración del archivo, o interrumpir el proceso con un mensaje al usuario.

Vamos a leer un archivo en el array numbers2.

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

Dado que se han escrito dos arrays diferentes en el archivo (no sólo numbers1, sino también xyz), se han leído 8 elementos en el array receptor (es decir, todo el archivo hasta el final, porque no se especificó lo contrario mediante parámetros).

De hecho, el tamaño de la estructura XYZ es de 16 bytes (4 campos de 4 bytes: un int y tres color), que corresponde a una fila del array numbers2 (2 elementos de tipo long). En este caso se trata de una coincidencia. Como ya se ha indicado, las funciones no tienen ni idea de la configuración y el tamaño de los datos en bruto y pueden leer cualquier cosa en cualquier array: el programador debe controlar la validez de la operación.

Vamos a comprobar los estados inicial y recibido. Array fuente numbers1:

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

Array resultante numbers2:

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

El comienzo del array numbers2 coincide completamente con el array original numbers1, es decir, la escritura y la lectura a través del archivo funcionan correctamente.

La última fila está ocupada en su totalidad por una única estructura XYZ (con valores correctos, pero representación incorrecta como dos números del tipo long).

Ahora llegamos al principio del archivo (utilizando la función FileSeek, de la que hablaremos más adelante en la sección Control de posición dentro de un archivo) y llamamos a FileReadArray indicando el número y la cantidad de elementos, es decir, realizamos una lectura parcial.

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

Se leen tres elementos del archivo y se colocan, empezando por el índice 10, en el array receptor numbers3. Como el archivo se lee desde el principio, estos elementos son los valores 1, 4, 2. Y como un array bidimensional tiene la configuración [][2], el índice pasante 10 apunta al elemento [5,0]. Este es el aspecto que tiene en la memoria:

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

Los elementos marcados en amarillo son aleatorios (pueden cambiar en diferentes ejecuciones de script). Es posible que todos sean cero, pero no está garantizado. El array numbers3 estaba inicialmente vacío y la llamada a FileReadArray inició una asignación de memoria necesaria para recibir 3 elementos en el offset 10 (13 en total). El bloque seleccionado no se rellena con nada, y sólo se leen 3 números del archivo. Por lo tanto, los elementos con índices de paso de 0 a 9 (es decir, las 5 primeras filas), así como el último, con índice 13, contienen basura.

Los arrays multidimensionales se escalan a lo largo de la primera dimensión y, por lo tanto, un aumento de 1 número significa añadir toda la configuración a lo largo de dimensiones superiores. En este caso, la distribución se refiere a una serie de dos números ([][2]). En otras palabras: el tamaño solicitado, 13, se redondea a un múltiplo de dos, es decir, 14.

Por último, vamos a probar cómo funcionan las funciones con arrays de cadenas. Vamos a crear un nuevo archivo o a abrir uno ya existente, volcando su contenido. A continuación, en dos llamadas a FileWriteArray, escribiremos los arrays text1 y 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);

El array de cadenas se guarda correctamente. El array numérico se ignora con un error FILE_NOTBIN(5011) porque debe abrir el archivo en modo binario.

Al intentar escribir un array de estructuras tt, obtenemos un error de compilación con un largo mensaje de «no se permiten estructuras o clases con objetos». Lo que el compilador quiere decir en realidad es que no le gustan los campos como string (se supone que las cadenas y los arrays dinámicos tienen una representación interna de algunos objetos de servicio). Así, a pesar de que el archivo se abre en modo texto y sólo hay campos de texto en la estructura, esta combinación no es compatible con MQL5.

   // COMPILATION ERROR: structures or classes containing objects are not allowed
   FileWriteArray(writertt);

La presencia de campos de cadena hace que la estructura sea «complicada» e inadecuada para trabajar con funciones FileWriteArray/FileReadArray en cualquier modo.

Después de ejecutar el script, puede cambiar al directorio MQL5/Files/MQL5Book y examinar el contenido de los archivos generados.

Anteriormente, en la sección Escritura y lectura de archivos en modo simplificado, hemos hablado de las funciones FileSave y FileLoad. En el script de prueba (FileSaveLoad.mq5) hemos implementado las versiones equivalentes de estas funciones utilizando FileWriteArray y FileReadArray. Pero no las hemos visto en detalle. Como ya estamos familiarizados con estas nuevas funciones, podemos examinar el código fuente:

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)));
   // this version has the following check added compared to the standard FileLoad:
   // if the file size is not a multiple of the structure size, print a warning
   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 se construye sobre una única llamada FileWriteArray, y MyFileLoad sobre una llamada FileReadArray, entre un par de llamadas FileOpen/FileClose. En ambos casos, se escriben y se leen todos los datos disponibles. Gracias a las plantillas, nuestras funciones también pueden aceptar arrays de tipos arbitrarios. Pero si algún tipo no soportado (por ejemplo, una clase) se deduce como metaparámetro T, se producirá un error de compilación, como ocurre con el acceso incorrecto a funciones integradas.