English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Recetas de MQL5 - implementamos el array asociativo o el diccionario para el acceso rápido a los datos

Recetas de MQL5 - implementamos el array asociativo o el diccionario para el acceso rápido a los datos

MetaTrader 5Ejemplos | 31 julio 2015, 12:56
1 342 0
Vasiliy Sokolov
Vasiliy Sokolov

Índice


Introducción

Este artículo describe una clase cómoda para almacenar la información: array asociativo o diccionario. Gracias a esta clase se puede acceder a la información por su clave.

El array asociativo recuerda un array común, pero en vez del índice utiliza una clave única, por ejemplo, la enumeración ENUM_TIMEFRAMES o cualquier texto. No importa qué clave es, lo importante es que esta clave sea única. Este algoritmo de almacenamiento de datos simplifica considerablemente muchos aspectos de programación.

Por ejemplo, la función para recibir el código del error y mostrar el equivalente textual de este error podría tener el siguiente aspecto:

//+---------------------------------------------------------------------+
//| Muestra en el terminal la descripción del error recibido.           |
//| Si el identificador del error se desconoce, muestra "Unknown error" |
//+---------------------------------------------------------------------+
void PrintError(int error)
 {
   Dictionary dict;
   CStringNode* node = dict.GetObjectByKey(error);
   if(node != NULL)
      printf(node.Value());
   else
      printf("Unknown error");
 }

Más tarde analizaremos la específica de este código.

Antes de pasar directamente a la descripción de la lógica interna del array asociativo, vamos a considerar con detalles dos modos principales de almacenamiento de datos: se trata de los arrays y de las listas. Nuestro diccionario va a basarse precisamente en estos dos tipos de datos, por eso tenemos que comprender muy bien las características específicas de su funcionamiento. El capítulo 1 está destinado a la descripción de los tipos de datos. El capítulo 2 describe el array asociativo y el modo de trabajo con él.


Capítulo 1. Teoría de organización de datos

1.1. Algoritmos de organización de datos. Búsqueda del contenedor óptimo para almacenar los datos

La búsqueda, almacenamiento y la representación de la información son las funciones más importantes impuestas al ordenador moderno. Por regla general, nuestra interacción con el ordenador consiste en la búsqueda de alguna información, o bien su creación y almacenamiento para el uso posterior. La información no es un concepto abstracto. En vida real, esta palabra supone un concepto concreto. Por ejemplo, para cualquier trader la fuente de información es el historial de cotizaciones del instrumento con el que ejecuta o planea ejecutar las transacciones. La documentación del lenguaje de programación o el código fuente de algún programa puede servir de la fuente de información para un programador.

Para las personas no relacionadas con la programación o el trading, la información puede ser un archivo gráfico, por ejemplo una foto hecha a través de su cámara digital. Es evidente que la información mencionada en estos ejemplos tiene la estructura diferente y su propia naturaleza. Por consiguiente, los algoritmos de almacenamiento, representación y procesamiento de esta información van a ser diferentes.

Por ejemplo, es más fácil representar el archivo gráfico como una matriz bidimensional (array bidimensional) cada elemento o la casilla de la cual va a guardar la información sobre el color del área pequeña de la imagen- píxel. Los datos sobre las cotizaciones tienen otra naturaleza. En realidad, es el flujo de datos homogéneos en el formato OHLCV. Es mejor representarlo como un array o una secuencia ordenada de estructuras que es un tipo especial de datos en el lenguaje de programación que combinan varios tipos de datos. La documentación o el código fuente es habitualmente un simple texto. Se puede representar y almacenar este tipo de datos como una secuencia ordenada de cadenas donde cada cadena es una secuencia arbitraria de caracteres.

Dependiendo del tipo de datos se elige uno u otro tipo del contenedor en el que se almacenan los datos. Es más fácil representar el contenedor para los datos en los terminales de la programación orientada a objetos como una clase que guarda dentro estos datos y posee los algoritmos (métodos) especiales que permiten manipularlos fácilmente. Existen varios contenedores (clases) para almacenar los datos. Ellos se basan en diferentes organizaciones de datos. No obstante, algunos algoritmos de datos permiten combinar diferentes paradigmas de almacenamiento de la información. De esta manera, se hace posible combinar las ventajas de todos los tipos de almacenamiento.

Usted elige uno u otro contenedor para almacenar, procesar y recibir los datos dependiendo del supuesto método de su manipulación y su naturaleza. Es importante comprender que no hay contenedores de datos igualmente eficaces. Los puntos débiles de un contenedor es el lado reverso de sus puntos fuertes.

Por ejemplo, en un array se puede obtener el acceso a cualquiera de sus elementos de forma rápida. Sin embargo, la inserción de un elemento en un lugar aleatorio del array es una operación compleja, ya que en este caso se requiere el redimensionamiento completo del array. Por otro lado, la inserción de un elemento en la lista simplemente enlazada es una operación eficaz y rápida, mientras que el acceso a un elemento aleatorio consume mucho tiempo. Si necesita insertar nuevos elementos a menudo y no necesita acceder a los elementos con mucha frecuencia, el contenedor apropiado será una lista simplemente enlazada. Si necesita acceder a los elementos aleatorios con frecuencia, la clase preferible para organizar los datos será un array.

Para comprender qué tipo de almacenamiento de datos hay que elegir, tiene que conocer bien la estructura de los contenedores. Este artículo está dedicado a la descripción del array asociativo o diccionario -un contenedor especial para almacenar los datos que se basa en la combinación de un array y una lista. No obstante, me gustaría mencionar que el diccionario puede ser implementado de diferentes maneras dependiendo de cada lenguaje de programación, sus medios, capacidades y normas de programación aceptadas.

Por ejemplo, la implementación de un diccionario en C# es diferente que en C++. No hay que esperar que este artículo describe la implementación adaptada del diccionario en С++. La versión del array asociativo descrita ha sido creada “desde cero” especialmente para el lenguaje de programación MQL5 tomando en cuenta las características especiales y la práctica de programación aceptada en este lenguaje. No obstante, las características generales de los diccionarios y los métodos de funcionamiento deben de ser parecidos a pesar de la implementación diferente. En este sentido, la versión descrita muestra por completo estas características y estos métodos.

Nosotros iremos creando el algoritmo del diccionario gradualmente, y al mismo tiempo iremos discutiendo sobre la naturaleza de los algoritmos de almacenamiento de datos. Al final del artículo, tendremos la versión del algoritmo completada estando plenamente conscientes del principio de su funcionamiento.

No hay contenedores igualmente eficaces para diferentes tipos de datos. Vamos a ver un simple ejemplo: una agenda de papel. También puede considerarse como un contenedor/clase en nuestra vida cotidiana. Todas las notas se hacen según una lista creada previamente (en este caso, alfabeto). Sabiendo el nombre del abonado, es muy fácil encontrar su número, porque sólo hay que abrir la agenda.


1.2. Arrays y direccionamiento directo a los datos

El array es la forma de almacenamiento de la información más simple y al mismo tiempo más eficaz según muchos parámetros. En la programación, el array es un conjunto de elementos homogéneos ubicados en la memoria uno tras otro. Gracias a estas propiedades se puede calcular la dirección de cada elemento del array.

En efecto, si todos los elementos son del mismo tipo, cada elemento tendrá el mismo tamaño que los demás. Puesto que los datos en el array se ubican continuamente, podemos calcular la dirección de un elemento aleatorio y referirse directamente a él si sabemos el tamaño del elemento básico. La fórmula general para calcular la dirección depende del tipo de datos y el índice.

Por ejemplo, podemos calcular la dirección del quinto elemento en el array de elementos del tipo uchar usando la siguiente fórmula general que se deriva de las propiedades de la organización de datos en el array:

dirección de un elemento aleatorio = dirección del primer elemento + (índice del elemento aleatorio en el array * tamaño del tipo del array)

El direccionamiento en los arrays se empieza desde cero por eso la dirección del primer elemento corresponde a la dirección del elemento del array con el índice 0. Por consecuencia, el quinto elemento tendrá el índice 4. Supongamos que el array guarda los elementos del tipo uchar, es el tipo básico de datos en muchos lenguajes de programación. Por ejemplo, está presente en todos los lenguajes del tipo C. MQL5 no es una excepción. Cada elemento del array del tipo uchar ocupa exactamente 1 byte o 8 bits de memoria.

Por ejemplo, según la fórmula anterior, la dirección del quinto elemento en el array tipo uchar será la siguiente:

dirección del quinto elemento = dirección del primer elemento + (4 * 1 byte);

En otras palabras, el quinto elemento del array uchar será 4 bytes mayor que el primer elemento. Durante la ejecución del programa Usted puede referirse a cada elemento del array usando directamente su dirección. Esta aritmética de direcciones permite acceder rápidamente a cada elemento del array. Pero esta organización de datos tiene sus desventajas.

Una de las desventajas del array es la imposibilidad de almacenar los elementos de diferentes tipos. Esta restricción es la consecuencia del direccionamiento directo. Pues, diferentes tipos de datos tienen tamaños diferentes por eso resulta imposible averiguar la dirección de un elemento determinado usando la fórmula de arriba. No obstante, se puede superar esta limitación de forma inteligente si Usted en vez de guardar en el array los elementos utiliza los punteros a ellos.

Lo más fácil es representar un puntero como un enlace (como accesos directos en Windows). El puntero hace referencia a un cierto objeto ubicado en la memoria, pero el puntero en sí no es un objeto. En MQL5 el puntero puede referirse sólo a una clase. En la programación orientada a objetos, la clase es un tipo de datos especial que puede incluir un conjunto arbitrario de datos y métodos y crearse por el usuario para la estructuración eficaz del programa.

Cada clase es un tipo de datos único definido por el usuario. Los punteros que hacen referencia a las clases diferentes no pueden ubicarse en el mismo array. Sin embargo, el puntero siempre tiene el mismo tamaño independientemente de la clase a la que se refiere, porque contiene sólo la dirección del objeto en el espacio de direcciones del sistema operativo que es común para todos los objetos ubicados.


1.3. Concepto del nodo con el ejemplo de la clase simple CObjectCustom

Ahora sabemos bastante para proyectar nuestro primer puntero universal. La idea es crear un array de punteros universales cada uno de los cuales se refiera a su tipo personal. Los tipos concretos serían diferentes, pero debido al hecho de que el mismo puntero se refiera a ellos, se podría guardarlos en el mismo contenedor/array. Vamos a crear la primera versión de nuestro puntero.

Este puntero será representado por la clase simple que vamos a llamar CObjectCustom:

class CObjectCustom
{
};

Sería muy complicado inventar la clase más simple que CObjectCustom, ya que CObjectCustom no contiene ningunos datos ni métodos. Pero por ahora esta implementación es suficiente.

Ahora usaremos una de las principales concepciones de la programación orientada a objetos: la herencia. La herencia permite establecer la identidad entre los objetos de una manera especial. Por ejemplo, podemos decir al compilador que cualquiera de las clases es derivada de CObjectCustom.

Por ejemplo, la clase de personas (СHuman), clase de Asesores Expertos (CExpert) y la clase del tiempo (CWeather) representan juntos los conceptos más generales de la clase CObjectCustom. Puede que en la vida real no haya ninguna relación entre estos conceptos: nada relaciona el tiempo con las personas y los Asesores Expertos con el tiempo. Sin embargo, en el mundo de la programación somos nosotros quienes establezcan las relaciones, y si estas relaciones son convenientes para nuestros algoritmos, nada nos impide crearlas.

De la misma manera, crearemos algunas clases más: clase del coche (CCar), clase que del número (CNumber), clase de la barra de precios (CBar), clase de cotizaciones (CQuotes), clase de la empresa MetaQuotes (CMetaQuotes) y la clase que describe el barco (CShip). Igual que con las clases anteriores, no hay una relación real entre ellas, pero todas ellas son derivadas de la clase CObjectCustom.

Vamos a crear la biblioteca de las clases para estos objetos uniendo todas estas clases en un solo archivo: ObjectsCustom.mqh:

//+------------------------------------------------------------------+
//|                                                ObjectsCustom.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Clase básico CObjectCustom                                       |
//+------------------------------------------------------------------+
class CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe las personas.                                 |
//+------------------------------------------------------------------+
class CHuman : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe el tiempo.                                    |
//+------------------------------------------------------------------+
class CWeather : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe el Asesor Experto.                            |
//+------------------------------------------------------------------+
class CExpert : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe el coche.                                     |
//+------------------------------------------------------------------+
class CCar : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe el número.                                    |
//+------------------------------------------------------------------+
class CNumber : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe la barra de precios.                          |
//+------------------------------------------------------------------+
class CBar : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe las cotizaciones.                             |
//+------------------------------------------------------------------+
class CQuotes : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe la empresa MetaQuotes.                        |
//+------------------------------------------------------------------+
class CMetaQuotes : public CObjectCustom
  {
  };
//+------------------------------------------------------------------+
//| Clase que describe el barco.                                     |
//+------------------------------------------------------------------+
class CShip : public CObjectCustom
  {
  };

Ha llegado el momento de unir todas estas clases separadas en un array.


1.4. Arrays de punteros a los nodos con el ejemplo de la clase CArrayCustom

Para combinar las clases, vamos a necesitar un array especial.

En el caso más simple, será suficiente escribir:

CObjectCustom array[];

Esta línea crea el array dinámico que guarda los elementos del tipo CObjectCustom. Debido a que todas las clases que hemos inventado en el apartado anterior proceden de CObjectCustom, podemos almacenar en este array cualquiera de estas clases. Por ejemplo, podemos colocar dentro las personas, los coches y los barcos, pero para hacerlo, una simple declaración del array CObjectCustom no será suficiente.

El hecho es que al declarar un array de manera clásica, todos sus elementos se llenan automáticamente en el momento de la inicialización del array. Así, después de declarar este array, todos los elementos en él estarán ocupados por la clase CObjectCustom.

Una pequeña modificación de CObjectCustom no ayudará a comprobarlo:

//+------------------------------------------------------------------+
//| Clase básico CObjectCustom                                       |
//+------------------------------------------------------------------+
class CObjectCustom
  {
public:

   void CObjectCustom()
     {
      printf("Object #"+(string)(count++)+" - "+typename(this));
     }
private:
   static int        count;
  };
static int CObjectCustom::count=0;

Vamos a ejecutar el código de prueba en forma del script para comprobar esta particularidad:

//+------------------------------------------------------------------+
//|                                                         Test.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectCustom array[3];
  }

En la función OnStart() hemos inicializado el array compuesto de tres elementos CObjectCustom.

El compilador llenará en seguida este array con los objetos correspondientes lo que podemos comprobar si leemos el log del terminal:

2015.02.12 12:26:32.964 Test (USDCHF,H1) Object #2 - CObjectCustom
2015.02.12 12:26:32.964 Test (USDCHF,H1) Object #1 - CObjectCustom
2015.02.12 12:26:32.964 Test (USDCHF,H1) Object #0 - CObjectCustom

Eso significa que el array se llena por el mimos compilador y nosotros ya no podemos colocar dentro otros elementos, por ejemplo, CWeather o CExpert.

Este código no será compilado:

#include "ObjectsCustom.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectCustom array[3];
   CWeather weather;
   array[0] = weather;
  }

El compilador mostrará el error:

'=' - structure have objects and cannot be copied       Test.mq5        18      13

Eso quiere decir que el array ya tiene los objetos y no se puede copiar nuevos objetos en él.

¡No obstante, esta situación tiene una solución! Como ya hemos dicho antes, para eso hay que trabajar con los punteros en vez de los objetos.

Reescribiremos el código de la función OnStart() para que pueda trabajar con los punteros:

#include "ObjectsCustom.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectCustom* array[3];
   CWeather* weather = new CWeather();
   array[0] = weather;
  }

Ahora el código se compila correctamente y los errores no aparecen. ¿Qué ha pasado? Hemos sustituido la inicialización del array CObjectCustom por la inicialización del array de punteros a CObjectCustom.

En este caso el compilador ya no crea nuevos objetos CObjectCustom al inicializar el array, sino los deja vacío. En segundo lugar, ahora usamos el puntero al objeto CWeather en vez del propio objeto. Usando la clave new hemos creado el objeto CWeather y lo hemos igualado a nuestro puntero weather y luego hemos colocado el puntero weather (no el objeto) en el array.

Ahora, de la misma manera, vamos a colocar en el array el resto de los objetos.

Para eso escribiremos el siguiente código:

#include "ObjectsCustom.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectCustom* arrayObj[8];
   arrayObj[0] = new CHuman();
   arrayObj[1] = new CWeather();
   arrayObj[2] = new CExpert();
   arrayObj[3] = new CCar();
   arrayObj[4] = new CNumber();
   arrayObj[5] = new CBar();
   arrayObj[6] = new CMetaQuotes();
   arrayObj[7] = new CShip();
  }

Este código va a funcionar pero es bastante peligroso porque estamos manipulando los índices de manera directa.

Basta equivocarse con el tamaño para nuestro array arrayObj o dirigirse a un índice erróneo, nuestro programa se terminará con un error crítico. Sin embargo, este código valdrá para nuestros fines de demostración.

Mostraremos estos elementos en el esquema:

Fig. 1. Esquema de datos en el array de punteros

Fig. 1. Esquema de datos en el array de punteros


Los elementos creados por el operador new se guardan en un lugar especial de la memoria operativa que se llama el montón. Como se ve en la imagen de arriba, están desordenados.

Nuestro array de punteros arrayObj tiene una indexación estricta lo que permite acceder muy rápido a cualquiera de sus elementos por su índice. Sin embargo, no es suficiente obtener el acceso a este elemento porque el array de punteros no sabe a que objeto en particular apunta. Es que el puntero CObjectCustom puede apuntar a CWeather, y a CBar, y a CMetaQuotes ya que todos ellos también son CObjectCustom. Por eso para obtener el tipo del elemento, es necesaria la conversión explícita del objeto.

Por ejemplo, se puede hacerlo así:

#include "ObjectsCustom.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectCustom* arrayObj[8];
   arrayObj[0] = new CHuman();
   CObjectCustom * obj = arrayObj[0];
   CHuman* human = obj;
  }

En este código hemos creado el objeto CHuman y lo hemos colocado en el array arrayObj como CObjectCustom. Luego hemos extraído CObjectCustom y lo hemos convertido en CHuman, lo que en realidad es. No ha ocurrido ningún error en este ejemplo ya que nosotros mismos hemos recordado el tipo. En una situación real, es muy difícil rastrear el tipo de cada objeto durante la programación porque pueden haber centenares de tipos y millones de objetos.

Por eso tenemos que proveer nuestra clase ObjectCustom con el método adicional Type() que devuelve el modificador del tipo actual de nuestro objeto. El modificador es un cierto número único que describe nuestro objeto permitiendo hacer referencia al tipo por su nombre. Por ejemplo, podemos definir los modificadores usando la directiva #define del preprocesador. Sabiendo el tipo del objeto indicado por el modificador, siempre se puede convertir su tipo al real de una manera segura. Pues bien, hemos acercado a la creación de tipos seguros.


1.5. Control y seguridad de los tipos

En cuanto empezamos a convertir un tipo a otro, la seguridad de esta conversión se hace la cuestión fundamental de nuestra programación. No nos gustaría que el programa se termine con un error crítico durante la ejecución. Ya hemos aclarado en qué va a basarse la seguridad de nuestros tipos: en los modificadores especiales. Sabiendo el modificador, podemos convertirlo al tipo necesario. Para eso necesitamos completar nuestra clase CObjectCustom con algunas adiciones.

En primer lugar, vamos a crear los identificadores de los tipos para referirse a ellos por el nombre. Para eso crearemos un archivo separado con las enumeraciones necesarias:

//+------------------------------------------------------------------+
//|                                                        Types.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"

#define TYPE_OBJECT     0     // Tipo común para todos CObjectCustom
#define TYPE_HUMAN      1     // Clase CHuman  
#define TYPE_WEATHER    2     // Clase CWeather
#define TYPE_EXPERT     3     // Clase CExpert
#define TYPE_CAR        4     // Clase CCar
#define TYPE_NUMBER     5     // Clase CNumber
#define TYPE_BAR        6     // Clase CBar
#define TYPE_MQ         7     // Clase CMetaQuotes
#define TYPE_SHIP       8     // Clase CShip

Ahora cambiaremos el código de las clases añadiendo en CObjectCustom una variable que guarda el tipo del objeto en forma de un número. Vamos a ocultarla en la sección private para que nadie pueda modificarla.

Además de eso, añadiremos un constructor especial disponible para las clases derivadas de CObjectCustom. Este constructor permitirá a los objetos indicar su tipo durante la creación.

El código será el siguiente:

//+------------------------------------------------------------------+
//| Clase básico CObjectCustom                                       |
//+------------------------------------------------------------------+
class CObjectCustom
  {
private:
   int               m_type;
protected:
                     CObjectCustom(int type){m_type=type;}
public:
                     CObjectCustom(){m_type=TYPE_OBJECT;}
   int Type(){return m_type;}
  };
//+------------------------------------------------------------------+
//| Clase que describe las personas.                                 |
//+------------------------------------------------------------------+
class CHuman : public CObjectCustom
  {
public:
                     CHuman() : CObjectCustom(TYPE_HUMAN){;}
   void Run(void){printf("Human run...");}
  };
//+------------------------------------------------------------------+
//| Clase que describe el tiempo.                                    |
//+------------------------------------------------------------------+
class CWeather : public CObjectCustom
  {
public:
                     CWeather() : CObjectCustom(TYPE_WEATHER){;}
   double Temp(void){return 32.0;}
  };
...

Como podemos ver, cada tipo derivado de CObjectCustom ahora define su propio tipo en su constructor durante la creación. Después de que el tipo haya sido definido, ya no se puede modificarlo porque el campo en el que se guarda es privado y nadie puede acceder a él salvo CObjectCustom. Eso nos salvará de las manipulaciones erróneas con los tipos. Si la clase no llama al constructor protected CObjectCustom, su tipo será el tipo predefinido - TYPE_OBJECT.

Bien, ha llegado la hora de aprender cómo extraer nuestros tipos de arrayObj y procesarlos correctamente. Para eso proveeremos las clases CHuman y CWeather con los métodos abiertos Run() y Temp(), respectivamente. Después de extraer la clase de arrayObj, la convertimos en el tipo necesario y empezamos a trabajar con ella de manera correspondiente.

Si el tipo que se guarda en el array CObjectCustom es desconocido para nosotros, lo ignoramos poniendo el mensaje correspondiente "unknown type":

#include "ObjectsCustom.mqh"
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectCustom* arrayObj[3];
   arrayObj[0] = new CHuman();
   arrayObj[1] = new CWeather();
   arrayObj[2] = new CBar();
   for(int i = 0; i < ArraySize(arrayObj); i++)
   {
      CObjectCustom* obj = arrayObj[i];
      switch(obj.Type())
      {
         case TYPE_HUMAN:
         {
            CHuman* human = obj;
            human.Run();
            break;
         }
         case TYPE_WEATHER:
         {
            CWeather* weather = obj;
            printf(DoubleToString(weather.Temp(), 1));
            break;
         }
         default:
            printf("unknown type.");
      }
   }
  }

Este código mostrará el siguiente mensaje:

2015.02.13 15:11:24.703 Test (USDCHF,H1) unknown type.
2015.02.13 15:11:24.703 Test (USDCHF,H1) 32.0
2015.02.13 15:11:24.703 Test (USDCHF,H1) Human run...

Bien, hemos conseguido el resultado deseado. Ahora podemos almacenar los objetos de cualquier tipo en el array CObjectCustom, obteniendo el acceso rápido a ellos por su índice en el array y convirtiéndolos correctamente a los tipos reales. No obstante, todavía nos faltan muchas cosas: necesitamos la eliminación correcta de objetos después de la finalización del programa ya que nosotros mismos tenemos que eliminar los objetos ubicados en el montón usando el operador delete.

Además, necesitamos un mecanismo seguro para redimensionar el array en caso de su llenado. Pero no vamos a inventar la bicicleta, la entrega estándar de MetaTrader 5 incluye las clases que implementan todas estas posibilidades.

Estas clases se basan en el contenedor/clase universal CObject. Igual que nuestra clase, tiene el método Type() que devuelve el tipo real de la clase, así como dos punteros importantes del tipo CObject: m_prev y m_next. Sobre su finalidad hablaremos en el siguiente apartado, donde discutiremos otro modo de almacenamiento de datos llamado lista doblemente enlazada, con el ejemplo del contenedor estándar CObject y la clase CList.


1.6. Clase CList como ejemplo de la lista doblemente enlazada

El array de elementos del tipo arbitrario tiene sólo un defecto importante: la inserción de un elemento nuevo en este array requiere mucho tiempo y esfuerzo, sobre todo si el elemento se inserta en el medio del array. Los índices van uno detrás del otro, por eso durante la inserción es necesario redimensionar el array para que el número total de elementos se aumente a uno más, luego hay que reorganizar todos los elementos que siguen el objeto insertado para que su índice corresponda al valor nuevo.

Supongamos que tenemos un array de 7 elementos y queremos insertar nuevo elemento en la cuarta posición. Entonces, el esquema aproximado será el siguiente:

Fig. 2. Esquema de redimensionamiento del array y la inserción del elemento nuevo

Fig. 2. Esquema de redimensionamiento del array y la inserción del elemento nuevo

Existe un esquema de almacenamiento de datos que permite insertar y eliminar los elementos de una manera rápida y eficaz. Este esquema se llama la lista simplemente enlazada o la lista doblemente enlazada. La lista recuerda una cola común. Cuando hacemos cola, es suficiente conocer a la persona detrás de la cual estamos (no hace falta saber detrás de quién está el que se encuentra delante de nosotros). Tampoco tenemos que saber quién está detrás de nosotros porque es él quien tiene que controlar su turno en la cola.

La cola es el ejemplo clásico de una lista simplemente enlazada. Pero las listas pueden ser doblemente enlazadas. En este caso, cada persona de la cola sabe no sólo quién está delante sino también quién está detrás. Entonces, si preguntamos a cada persona, podemos movernos tanto desde el final de la cola hasta su inicio, como desde el inicio hasta el final.

La CList estándar que está disponible en la Biblioteca estándar nos ofrece precisamente este algoritmo. Vamos a intentar crear una cola compuesta por las clases bien conocidas, pero ahora van a derivarse de CObject estándar en vez de nuestra CObjectCustom.

Podemos esquematizarlo de la siguiente manera:

Fig. 3. Esquema de organización de una lista doblemente enlazada

Fig. 3. Esquema de organización de una lista doblemente enlazada

Pues, aquí tenemos el código fuente que crea este esquema:

//+------------------------------------------------------------------+
//|                                                     TestList.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Object.mqh>
#include <Arrays\List.mqh>

class CCar : public CObject{};
class CExpert : public CObject{};
class CWealth : public CObject{};
class CShip : public CObject{};
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CList list;
   list.Add(new CCar());
   list.Add(new CExpert());
   list.Add(new CWealth());
   list.Add(new CShip());
   printf(">>> enumerate from begin to end >>>");
   EnumerateAll(list);
   printf("<<< enumerate from end to begin <<<");
   ReverseEnumerateAll(list);
  }

Nuestras clases ahora adquieren dos punteros de CObject: un puntero hace referencia al elemento anterior y otro al siguiente. El elemento del inicio de la lista tiene el puntero al elemento anterior igual a NULL. El elemento del final de la lista tiene el puntero al siguiente elemento igual a NULL. Sabiendo eso, podemos repasar los elementos uno a uno repasando así la cola entera.

Las funciones EnumerateAll() y ReverseEnumerateAll() se encargan del repaso de todos los elementos de la lista.

La primera repasa la lista desde el inicio hasta el final, la segunda desde el final hasta el inicio. Es el código fuente de estas funciones:

//+-----------------------------------------------------------------------------+
//| Repasa la lista desde el inicio hasta el final mostrando el número de orden |
//| de cada elemento en el terminal.                                            |
//+-----------------------------------------------------------------------------+
void EnumerateAll(CList& list)
{
   CObject* node = list.GetFirstNode();
   for(int i = 0; node != NULL; i++, node = node.Next())
      printf("Element at " + (string)i); 
}
//+-----------------------------------------------------------------------------+
//| Repasa la lista desde el final hasta el inicio mostrando el número de orden |
//| de cada elemento en el terminal.                                            |
//+-----------------------------------------------------------------------------+
void ReverseEnumerateAll(CList& list)
{
   CObject* node = list.GetLastNode();
   for(int i = list.Total()-1; node != NULL; i--, node = node.Prev())
      printf("Element at " + (string)i); 
}

¿Cómo funciona este código? En realidad, es muy simple. Al principio, obtenemos la referencia al primer nodo en la función EnumerateAll(). Luego imprimimos el número de orden de este nodo en el ciclo for y pasamos al siguiente nodo usando el comando node = node.Next(), sin olvidar iterar el índice actual del elemento a uno (i++). El repaso continúa hasta que el nodo actual node sea igual a NULL, de eso se encarga el código del segundo bloque for: node != NULL.

La versión reversa de esta función ReverseEnumerateAll() funciona de la misma manera con la diferencia de que al principio ella recibe el último elemento de la lista CObject* node = list.GetLastNode(). En el ciclo for ella obtiene el elemento anterior de la lista node = node.Prev().

Después de iniciar este código, recibiremos el siguiente mensaje:

2015.02.13 17:52:02.974 TestList (USDCHF,D1) enumerate complete.
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 0
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 1
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 2
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 3
2015.02.13 17:52:02.974 TestList (USDCHF,D1) <<< enumerate from end to begin <<<
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 3
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 2
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 1
2015.02.13 17:52:02.974 TestList (USDCHF,D1) Element at 0
2015.02.13 17:52:02.974 TestList (USDCHF,D1) >>> enumerate from begin to end >>>

Usted puede insertar fácilmente nuevos elementos en la lista. Para eso será suficiente cambiar los punteros del elemento anterior y del siguiente de tal manera que hagan referencia al elemento nuevo y este elemento nuevo haga referencia al objeto anterior y al siguiente.

En el esquema eso se ve más fácil que en la descripción:

Fig. 4. Inserción del nuevo elemento en la lista doblemente enlazada

Fig. 4. Inserción del nuevo elemento en la lista doblemente enlazada


La principal desventaja de la lista es la imposibilidad de dirigirse a cada uno de sus elementos por su índice.

Por ejemplo, para dirigirse a CExpert de la Fig. 4, primero hay que obtener el acceso a CCar y luego pasar a CExpert. Lo mismo pasa con CWeather. Se encuentra más cerca del final de la lista que de su inicio por eso es más fácil acceder a él desde el final. Para eso primero hay que dirigirse a CShip, y luego a CWeather.

Moverse por los punteros es una operación más lenta en comparación con la indexación directa. Los procesadores centrales modernos están optimizados para operar particularmente con los arrays. Por eso en la práctica a veces los arrays pueden ser más preferibles incluso si en teoría la lista debe trabajar más rápido.


Capítulo 2. Teoría de organización del array asociativo

2.1. Arrays asociativos en la vida cotidiana

En nuestra vida cotidiana estamos conviviendo con los arrays asociativos. No obstante, su funcionamiento nos parece tan evidente que los percibimos como algo muy natural. El ejemplo más simple de un array asociativo o diccionario es la guía de teléfonos habitual. Cada número de teléfono de esta guía está asociado con el nombre de una determinada persona. En esta guía el nombre de la persona es la clave única y el número de teléfono es un simple valor numérico. Cada contacto de la guía telefónica puede tener más de un número de teléfono. Por ejemplo, una persona puede tener el número de teléfono de casa, el número móvil y el número de teléfono de trabajo.

De modo general, puede haber una cantidad ilimitada de números, pero el propio contacto es único. Si su guía de teléfonos va a contener, por ejemplo, dos contactos con el nombre “Alejandro”, eso va a confundirnos. De vez en cuando vamos a marcar el número incorrecto. Para que eso no ocurra, las claves (en este caso los nombres) tienen que ser únicas. Pero al mismo tiempo el diccionario tiene que saber solucionar las colisiones y ser resistente frente a su aparición. Si en la guía de teléfonos aparecen dos nombres iguales, eso no va a destruirla. De la misma manera, nuestro algoritmo tiene que saber enfrentarse a estas situaciones.

En la vida real usamos varios tipos de diccionarios. Por ejemplo, la guía telefónica es un diccionario donde la línea única (nombre del contacto) es una clave, y el número es un valor. El diccionario de términos extranjeros tiene una estructura algo diferente. En él la clave es una palabra en inglés, y su traducción es el valor. Ambos arrays asociativos se basan en los mismos métodos de procesamiento los datos, por eso nuestro diccionario tiene que ser universal y permitir almacenar y comparar todos los tipos de datos.

En la programación, también puede ser muy conveniente crear sus propios diccionarios y “libretas de notas”.


2.2. Arrays asociativos simplísimos a base del operador switch-case o array simple

Usted puede crear fácilmente un array asociativo simple. Bastará usar las herramientas estándar del lenguaje MQL5, por ejemplo, el operador switch o el array.

Vamos a analizar detalladamente este código:

//+------------------------------------------------------------------+
//| Devuelve la representación string del período dependiendo del    |
//| marco temporal traspasado.                                       |
//+------------------------------------------------------------------+
string PeriodToString(ENUM_TIMEFRAMES tf)
{
   switch(tf)
   {
      case PERIOD_M1:
         return "M1";
      case PERIOD_M5:
         return "M5";
      case PERIOD_M15:
         return "M15";
      case PERIOD_M30:
         return "M30";
      case PERIOD_H1:
         return "H1";
      case PERIOD_H4:
         return "H4";
      case PERIOD_D1:
         return "D1";
      case PERIOD_W1:
         return "W1";
      case PERIOD_MN1:
         return "MN1";
   }
   return "unknown";
}

En este caso, el operador switch-case actúa como un diccionario. Cada valor ENUM_TIMEFRAMES tiene un valor de cadena de caracteres que describe este período. Debido a que el operador switch es un paso conmutado (en ruso), el acceso a la variante solicitada case es inmediato, otras variantes case no se repasan. Por eso este código tiene una alta eficiencia.

No obstante, su desventaja consiste en que, primero, Usted tiene que rellenar manualmente todos los valores que hay que devolver con uno u otro valor de ENUM_TIMEFRAMES. La segunda desventaja es que switch trabaja sólo con las variables de números enteros. Sería más complicado escribir la función inversa que podría devolver el tipo del período de tiempo dependiendo de la cadena transferida. La tercera desventaja es que este método no es suficientemente flexible. Es necesario especificar todas las opciones posibles de antemano. Sin embargo, a menudo surge la necesidad de rellenar los valores en el diccionario dinámicamente, a medida de que vayan llegando los datos.

El segundo método “frontal” de almacenamiento de los pares “clave-valor” consiste en la creación de un array donde la clave se usa como el índice, y el valor se usa como el elemento del array.

Por ejemplo, intentaremos resolver la tarea semejante: devolver la representación string del período de tiempo.

//+------------------------------------------------------------------+
//| Aquí se guardan los valores string que corresponden              |
//| al período de tiempo.                                            |
//+------------------------------------------------------------------+
string tf_values[];
//+------------------------------------------------------------------+
//| Añade valores asociativos al array.                              |
//+------------------------------------------------------------------+
void InitTimeframes()
{
   ArrayResize(tf_values, PERIOD_MN1+1);
   tf_values[PERIOD_M1] = "M1";
   tf_values[PERIOD_M5] = "M5";
   tf_values[PERIOD_M15] = "M15";
   tf_values[PERIOD_M30] = "M30";
   tf_values[PERIOD_H1] = "H1";
   tf_values[PERIOD_H4] = "H4";
   tf_values[PERIOD_D1] = "D1";
   tf_values[PERIOD_W1] = "W1";
   tf_values[PERIOD_MN1] = "MN1";
}
//+------------------------------------------------------------------+
//| Devuelve la representación string del período dependiendo del    |
//| marco temporal traspasado.                                       |
//+------------------------------------------------------------------+
void PeriodToStringArray(ENUM_TIMEFRAMES tf)
{
   if(ArraySize(tf_values) < PERIOD_MN1+1)
      InitTimeframes();
   return tf_values[tf];
}

Este código representa la referencia por el índice donde la enumeración ENUM_TIMFRAMES está especificada como el índice. Antes de devolver el valor, la función comprueba si la función está llenada con los elementos necesarios, Si no es así, delega el relleno a la función especial InitTimeframes(). Este método tiene las mismas desventajas que el operador switch.

Además, esta estructura del diccionario requiere la inicialización del array con valores muy grandes. Pues, el valor del modificador PERIOD_MN1 es igual a 49153. Eso significa que es necesario asignar 49153 casillas para almacenar sólo nueve períodos de tiempo. Todas las demás casillas quedan no llenadas. Este método de ubicación de datos está lejos de ser compacto. Sin embargo, puede ser conveniente cuando la enumeración ocupa una pequeña y sucesiva serie de números.


2.3. Conversión de tipos básicos a la clave única

Puesto que los algoritmos utilizados en el diccionario son los mismos independientemente de los tipos concretos de la clave y valor, tenemos que realizar la unificación de datos para que diferentes tipos de datos se procesen por un solo algoritmo. Nuestro algoritmo del diccionario será bastante universal y permitirá almacenar los valores donde cualquier tipo básico podrá ser especificado como una clave, por ejemplo int, enum, double, e incluso string.

La verdad es que en MQL5 cualquier tipo básico de datos puede representarse como algún número sin signo ulong. Por ejemplo, el tipo de datos short o ushort es la versión “recortada” de ulong.

Suponiendo que en el tipo ulong se guarda el valor ushort, siempre se puede hacer la conversión de tipos explícita y segura:

ulong ln  = (ushort)103; // Guardamos el valor ushort en el tipo ulong (103)
ushort us = (ushort)ln;  // Obtenemos el valor ushort desde el tipo ulong (103)

Lo mismo se refiere a los tipos char e int, así como a sus análogos sin signos. Es que en ulong podemos guardar cualquier tipo cuyo tamaño es menor o igual a ulong. Los tipos datetime, enum y color se basan en el número entero de 32 bits uint, lo que significa que podemos convertirlos al ulong de forma segura. El tipo bool adquiere sólo dos valores: 0 que significa false y 1 que significa true. De esa manera, los valores del tipo bool también pueden almacenarse en la variable del tipo ulong. Por cierto, precisamente en esta propiedad se basan muchas funciones de sistema en MQL5.

Por ejemplo, la función AccountInfoInteger() en realidad devuelve el número entero del tipo long que puede ser un número de la cuenta del tipo ulong y un valor booleano del permiso para tradear ACCOUNT_TRADE_ALLOWED.

No obstante, en MQL5 hay tres tipos básicos que se distinguen de los tipos enteros. No se puede convertirlos directamente al tipo entero ulong. Se trata de los tipos que representan los números con punto flotante, tales como double y float, así como las cadenas (strings). Sin embargo, con algunas acciones simples se puede convertirlos a las claves-valores únicas enteras.

Cada número con punto flotante puede ser representado como la base elevada a una potencia aleatoria donde la base y la potencia se guardan por separado como valores enteros. Aproximadamente, este método de almacenamiento se utiliza en los valores double y float. El tipo float utiliza 32 dígitos para almacenar la mantisa y la potencia, y el tipo double utiliza 64 dígitos.

Si intentamos convertirlos directamente al tipo ulong, el número simplemente será redondeado. En este caso, los números 3,0 y 3,14159 darán el mismo número ulong: 3. Eso no nos vale porque vamos a necesitar dos claves diferentes para estos dos número absolutamente diferentes. Nos puede ayudar una propiedad no trivial que puede ser utilizada en los lenguajes del tipo C. Se llama la conversión de estructuras. Si dos estructuras diferentes tienen el mismo tamaño, se puede convertirlas una a otra.

Analizaremos este ejemplo con dos estructuras una de las cuales guarda el valor del tipo ulong, y la segunda es del tipo double:

struct DoubleValue{ double value;} dValue;
struct ULongValue { ulong value; } lValue;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   dValue.value = 3.14159;
   lValue = (ULongValue)dValue;
   printf((string)lValue.value);
   dValue.value = 3.14160;
   lValue = (ULongValue)dValue;
   printf((string)lValue.value);
  }

Este código copia la estructura DoubleValue en la estructura ULongValue byte por byte. Dado que tienen el mismo tamaño y el orden de las variables, el valor double de dValue.value se copia byte por byte en la variable ulong de lValue.value.

Después de eso, el valor de esta variable se imprime. Si cambiamos el valor de dValue.value por 3,14160, el valor de lValue.value también se cambiará.

Es el resultado que mostrará la función printf():

2015.02.16 15:37:50.646 TestList (USDCHF,H1) 4614256673094690983
2015.02.16 15:37:50.646 TestList (USDCHF,H1) 4614256650576692846

El tipo float es la versión corta del tipo double. Entonces, antes de convertir el tipo float al tipo ulong, primero se puede extender con total seguridad el tipo float hasta double:

float fl = 3.14159f;
double dbl = fl;

Después de eso, hay que convertir double al tipo ulong mediante la conversión de estructuras.


2.4. Dispersión (Hashing) de cadenas y el uso del hash como clave

En los ejemplos mencionados antes, las claves se representaban por los mismos tipos de datos, para ser más exacto, por las cadenas de caracteres (strings). Sin embargo, no siempre puede ser así. Por ejemplo, tres primeros dígitos de un número de teléfono indican al operador de la red telefónica al que pertenece este número. En este caso, precisamente estos tres dígitos representan la clave. Por otro lado, cada cadena puede representarse como un número único donde cada dígito significa el número de la letra en el alfabeto. De esta manera, se puede convertir la cadena al número único y utilizar este número como la clave entera para el valor asociado con él.

Este método es bueno, pero tampoco es suficientemente universal. Si usamos una cadena muy larga con centenares de caracteres como la clave, el número va a ser increiblemente grande. Va a ser imposible meterlo dentro de una simple variable de cualquier tipo. Para resolver los problemas de este tipo, se usan las funciones hash. La función hash es un algoritmo especial que acepta cualquier tipo de datos (por ejemplo, una cadena) y devuelve un número único que caracteriza esta cadena.

Si incluso un carácter de los datos de entrada ha sido cambiado, el número será absolutamente diferente. Los números devueltos por esta función tienen un rango fijo. Por ejemplo, la función hash Adler32() acepta como parámetro una cadena arbitraria y devuelve un número en el rango de 0 a 2 elevado a la potencia 32 . Es una función bastante simple pero nos valdrá para nuestras tareas.

Aquí tiene su código fuente en MQL5:

//+------------------------------------------------------------------+
//| Acepta una cadena y devuelve un valor hash de 32 bits            |
//| que caracteriza esta cadena.                                     |
//+------------------------------------------------------------------+
uint Adler32(string line)
{
   ulong s1 = 1;
   ulong s2 = 0;
   uint buflength=StringLen(line);
   uchar char_array[];
   ArrayResize(char_array, buflength,0);
   StringToCharArray(line, char_array, 0, -1, CP_ACP);
   for (uint n=0; n<buflength; n++)
   {
      s1 = (s1 + char_array[n]) % 65521;
      s2 = (s2 + s1)     % 65521;
   }
   return ((s2 << 16) + s1);
}

Vamos a ver qué números devuelve dependiendo de la cadena transferida.

Para eso escribiremos un simple script que llama a esta función y le traspasa cadenas diferentes:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   printf("Hello world - " +  (string)Adler32("Hello world"));
   printf("Hello world! - " +  (string)Adler32("Hello world!"));
   printf("Peace - " +  (string)Adler32("Peace"));
   printf("MetaTrader - " +  (string)Adler32("MetaTrader"));
  }

El script ha mostrado lo siguiente:

2015.02.16 13:29:12.576 TestList (USDCHF,H1) MetaTrader - 352191466
2015.02.16 13:29:12.576 TestList (USDCHF,H1) Peace - 91685343
2015.02.16 13:29:12.576 TestList (USDCHF,H1) Hello world! - 487130206
2015.02.16 13:29:12.576 TestList (USDCHF,H1) Hello world - 413860925

Como podemos ver, a cada cadena le corresponde un número único. Preste atención en las cadenas "Hello world" y "Hello world!" - son prácticamente idénticas a diferencia del signo exclamatorio al final de la segunda.

Sin embargo, los números que hemos obtenido a través de Adler32() en ambos casos han sido absolutamente difirentes.

Hemos aprendido a convertir el tipo string a un número sin signo uint y ahora en vez de la clave del tipo string podemos guardar su hash entero. Si dos cadenas tienen el mismo hash, es más probable que sea la misma cadena. De esta manera, la clave para el valor no es una cadena, sino su hash entero que va a generarse a base de esta cadena.


2.5. Cálculo del índice por la clave. Array de listas

Bueno, ahora sabemos cómo convertir cualquier tipo básico de MQL5 al tipo sin signo ulong. Precisamente él va a servirnos de la clave real al que nuestro valor va a corresponder. Sin embargo, no es suficiente tener la clave única del tipo ulong. Desde luego, sabiendo la clave única de cada objeto, podríamos organizar algún método primitivo de su almacenamiento a base del operador switch-case o un array de longitud arbitraria.

Estos métodos han sido descritos en la sección 2.2 del presente capítulo. No obstante, todos ellos no son suficientemente flexibles y eficientes. En cuanto al operador switch-case, no se puede describir todas las variantes en este operador.

Si hay decenas de miles de objetos, tendremos que describir el operador switch compuesto de decenas de miles de claves en la fase de compilación. Eso es imposible. El segundo método es el almacenamiento en el array donde la clave del elemento es su índice. Eso permitirá en caso de necesidad redimensionar el array dinámicamente agregando los elementos necesarios. De este modo, podemos referirnos constantemente a él por su índice, donde el índice del elemento será su clave.

Vamos a hacer un anteproyecto de esta solución:

//+------------------------------------------------------------------+
//| Array para almacenar las cadenas por la clave.                   |
//+------------------------------------------------------------------+
string array[];
//+------------------------------------------------------------------+
//| Añade una cadena en el array asociativo.                         |
//| RESULTS:                                                         |
//|   Devuelve true si se ha podido añadir la cadena, y false en     |
//|   caso contrario.                                                |
//+------------------------------------------------------------------+
bool AddString(string str)
  {
   ulong key=Adler32(str);
   if(key>=ArraySize(array) && 
      ArrayResize(array,key+1)<=key)
      return false;
   array[key]=str;
   return true;
  }

En realidad, este código genera más problemas que resuelve. Supongamos que la función hash mostrará un hash muy grande. Como en el ejemplo dado el índice del array es igual a su hash, tendremos que redimensionar el array haciéndolo de un tamaño gigantesco. Y eso no significa que podremos conseguir hacerlo. ¿Está dispuesto a guardar la única cadena de caracteres en el contenedor cuyo tamaño en la memoria operativa va a llegar a varios gigabytes?

El segundo problema es que en caso de la colisión el valor anterior simplemente será sobrescrito con uno nuevo. Es que no se puede excluir la posibilidad de que la función Adler32() devuelva la misma clave hash para dos cadenas diferentes. ¿Está dispuesto a perder una parte de sus datos para poder referirse a ellos más rápido usando la clave? La respuesta a estas preguntas es evidente, ¡claro que no! Con el fin de evitar estas situaciones, es necesario modificar el algoritmo de almacenamiento y desarrollar un contenedor híbrido especial a base del array de listas.

El array de listas combina las mejores características de un array y una lista. Estas dos clases ya están presentes en la Biblioteca estándar. Vamos a recordar que los arrays permiten referirse a sus elementos aleatorios de una manera muy rápida pero su redimensión va a un ritmo muy lenta. Las listas, al contrario, permiten añadir y eliminar los elementos muy rápido pero el acceso a sus elementos es una operación muy lenta.

Podemos representar el array de listas en el siguiente esquema:

Fig. 5. Esquema del array de listas

Fig. 5. Esquema del array de listas

Del esquema de arriba se ve que el array de listas es un array donde cada uno de sus elementos está formado por una lista. Vamos a ver qué ventajas ofrece este esquema. En primer lugar, podemos referirnos rápidamente a cualquiera de las listas por su índice. En segundo lugar, si vamos a almacenar cada elemento de nuestros datos en una lista, también podremos añadir y eliminar rápidamente los elementos sin tener que redimensionar el array. El índice del array puede estar vacío o igual a NULL. Eso significa que los elementos correspondientes a este índice todavía no han sido añadidos.

La combinación de un array y una lista ofrece otra posibilidad no trivial. Podemos almacenar dos y más elementos usando un índice. Para comprender para qué es necesario, supongamos que necesitamos almacenar 10 números en un array de tres elementos. Es obvio que hay más números que los elementos en el array. Vamos a resolver este problema si organizamos el almacenamiento en el array de listas. Supongamos que necesitaremos una de tres listas asignadas a uno de tres índices del array para almacenar uno u otro número.

Para determinar el índice de la lista, tenemos que encontrar el resto de la división de nuestro número por la cantidad de elementos en el array:

índice del array = número % elementos en el array;

Por ejemplo, el índice de la lista para el número 2 será el siguiente: 2 % 3 = 2. Entonces, el número 2 va a guardarse en la lista con el índice 2. El número 3 va a guardarse en la lista con el índice 3%3 = 0. El número 7 va a guardarse en la lista con el índice 7%3 = 1. Una vez determinado el índice de la lista, basta añadir el número al final de esta lista.

Para extraer un número de la lista, hay que realizar las acciones parecidas. Supongamos que queremos extraer el número 7 de nuestro contenedor. Para eso determinaremos en qué lista tenemos que buscarlo: 7%3=1. Después de determinar que 7 puede estar en la lista con el índice 1, hay que repasar toda la lista, y si uno de sus elementos es igual a 7, devolver este valor.

Si podemos almacenar varios elementos usando un índice, no necesitamos crear los arrays gigantescos para almacenar una pequeña cantidad de datos. Supongamos que necesitamos almacenar el número 232 547 879 en un array compuesto por 0-10 dígitos y que tiene tres elementos. Este número también tendrá su índice en la lista que será igual a 2 (232 547 879 % 3 = 2).

Si sustituimos los números por los hash, podremos calcular el índice para cualquier elemento que necesitamos ubicar en el diccionario. Porque el hash es un número. Además de eso, gracias a la posibilidad de almacenar varios elementos en una lista, no necesitamos que el hash sea único. Los elementos con los hash iguales serán colocados en la misma lista. Si necesitamos extraer un elemento usando su clave, simplemente compararemos estos elementos y extraeremos el que va a corresponder a la clave.

Es posible porque dos elementos con los hash iguales tendrán dos claves únicas diferentes. La singularidad de las claves puede controlarse por la función que añade el elemento en el diccionario. Simplemente no va a insertar nuevo elemento si la clave a la que corresponde ya existe en el diccionario. Eso parece al control de la correspondencia de sólo un abonado a sólo un número de teléfono.


2.6. Redimensionamiento del array, minimización de la longitud de las listas

Cuanto más pequeño sea nuestro array de listas y cuanto más grande sea el número de los elementos añadidos, las cadenas de listas más largas serán formadas por nuestro algoritmo. Como ya ha sido dicho en el primer capítulo, el acceso a un elemento de esta cadena es una operación no eficaz. Cuanto más corta sea nuestra lista, más parecido será nuestro contenedor a un array que realiza el acceso a cada uno de sus elemento usando el índice. Tenemos que apuntar a las listas cortas y arrays largos. El caso ideal para diez elementos será un array compuesto de diez listas cada una de las cuales incluirá un elemento.

La peor opción será un array de diez elementos compuesto de una sola lista. Ya que todos los elementos van a añadirse a nuestro contenedor dinámicamente durante la ejecución del programa, no podemos saber de antemano este número de elementos. Por eso necesitamos repensar en el mecanismo del redimencionamiento dinámico del array, y además, hacer que el número de las cadenas en las listas tienda a un elemento. Es evidente que para eso es necesario mantener el tamaño del array igual al número total de elementos. La adición constante de los elementos requerirá el redimencionamiento continuo del array

La situación también se complica por el hecho de que, aparte del redimencionamiento del array, habrá que redimensionar todas las listas porque el índice al que puede pertenecer un elemento puede variarse tras el redimencionamiento. Por ejemplo, si el array contiene tres elementos, el número 5 va a almacenarse en el segundo índice (5%3 = 2). Si hay seis elementos, el número 5 ya estará en el quinto índice (5%6=5). De esta manera, el redimensionamiento del diccionario es una operación muy lenta. Hay que ejecutarlo cuanto menos. Por otro lado, si no lo hacemos en absoluto, con cada nuevo elemento las cadenas van a alargarse y la eficacia de acceso a cada elemento va a reducirse.

Podemos crear un algoritmo que implementa un compromiso razonable entre el número de redimensionamientos del array y la longitud media de la cadena. Se basará en que cada siguiente redimensionamiento va a duplicar el tamaño actual del array. Si el tamaño inicial es de dos elementos, el primer redimensionamiento lo aumentará a 4 (2^2), el segundo a 8 (2^3), el tercero a 16 (2^3), el decimosexto redimensionamiento asignará el tamaño para 65 536 cadenas (2^16). Cada redimensionamiento va a ejecutarse cuando el número de elementos añadidos coincidirá con el exponente ordinario de dos. De esta manera, el tamaño total de la memoria operativa actual necesaria no va a superar más de dos veces la memoria libre necesaria para almacenar todos los elementos. Por otro lado, la ley logarítmica permitirá evitar los redimensionamientos frecuentes del array.

Análogamente, según se vayan eliminando los elementos de la lista, podemos reducir el tamaño del array ahorrando por lo tanto el tamaño de la memoria operativa asignada.


Capítulo 3. Desarrollo práctico del array asociativo

3.1. Clase CDictionary a base de las plantillas, métodos AddObject, DeleteObjectByKey y contenedor KeyValuePair

El array asociativo que estamos creando tiene que ser bastante universal y permitir trabajar con las claves de cualquier tipo. Al mismo tiempo, vamos a utilizar los objetos a base de CObject estándar como valores. Ya que podemos colocar cualquier variable básica en las clases, nuestro diccionario será una solución universal. Por supuesto, podríamos crear varias clases de diccionarios. Para cada tipo básico se usaría su tipo de la clase.

Por ejemplo, podríamos crear las clases siguientes:

CDictionaryLongObj    // Para almacenar pares <ulong, CObject*>
CDictionaryCharObj    // Para almacenar pares <char, CObject*>
CDictionaryUcharObj   // Para almacenar pares <uchar, CObject*>
CDictionaryStringObj  // Para almacenar pares <string, CObject*>
CDictionaryDoubleObj  // Para almacenar pares <double, CObject*>
...

No obstante, en MQL5 hay demasiados tipos básicos. Además, tendríamos que corregir cada error muchas veces ya que todos los tipos tienen el mismo código principal. Usaremos las plantillas para evitar dplicaciones. Tendremos sólo una clase pero va a procesar varios tipos de datos simultáneamente. Por eso los métodos principales de la clase serán de plantilla.

Para empezar, vamos a crear nuestro primer método de plantilla Add(). Este método va a añadir el elemento con la clave aleatoria a nuestro diccionario. La clase del diccionario tendrá el nombre CDictionary. Aparte del elemento, el diccionario va a incluir el array de punteros a las listas CList, en estas cadenas vamos a almacenar los propios elementos:

//+-------------------------------------------------------------------+
//| Array asociativo o diccionario que guarda los elementos como      |
//| <clave - valor>, donde la clave puede ser cualquier tipo básico y |
//| y el valor puede representarse por un objeto CObject.             |
//+-------------------------------------------------------------------+
class CDictionary
  {
private:
   CList            *m_array[];       // Array de listas.
   template<typename T>
   bool              AddObject(T key,CObject *value);
  };
//+---------------------------------------------------------------------+
//| Añade en el diccionario un elemento tipo CObject con la clave T key |
//| INPUT PARAMETRS:                                                    |
//|   T key - cualquier tipo básico, por ejemplo, int, double o string. |
//|   value - clase derivada de CObject.                                |
//| RETURNS:                                                            |
//|   true, si el elemento ha sido añadido, en caso contrario, false.   |
//+---------------------------------------------------------------------+
template<typename T>
bool CDictionary::AddObject(T key,CObject *value)
  {
   if(ContainsKey(key))
      return false;
   if(m_total==m_array_size){
      printf("Resize" + m_total);
      Resize();
   }
   if(CheckPointer(m_array[m_index])==POINTER_INVALID)
     {
      m_array[m_index]=new CList();
      m_array[m_index].FreeMode(m_free_mode);
     }
   KeyValuePair *kv=new KeyValuePair(key, m_hash, value);
   if(m_array[m_index].Add(kv)!=-1)
      m_total++;
   if(CheckPointer(m_current_kvp)==POINTER_INVALID)
     {
      m_first_kvp=kv;
      m_current_kvp=kv;
      m_last_kvp=kv;
     }
   else
     {
      m_current_kvp.next_kvp=kv;
      kv.prev_kvp=m_current_kvp;
      m_current_kvp=kv;
      m_last_kvp=kv;
     }
   return true;
  }

El método AddObject() funciona de la siguiente manera. Primero comprueba si el diccionario contiene un elemento con la clave que hay que añadir. De esta comprobación se encarga el método ContainsKey(). Si la clave key ya existe en el diccionario, el nuevo elemento no se añade porque eso causará una ambigüedad dado que dos elementos van a corresponder a la misma clave.

Luego el método averiguará el tamaño del array en el que se almacenan las cadenas CList, y si el tamaño del array coincide con el número de los elementos, ha llegado la hora de realizar el redimensionamiento. El método Resize() realizará esta tarea.

Los siguientes pasos son sencillos. Si el índice calculado todavía no tiene ninguna cadena CList, esta cadena se crea. Previamente, el índice se calcula usando el método ContainsKey(). Él escribe el índice calculado en la variable m_index. Luego el método añade el elemento nuevo al final de la lista. Pero antes de eso, el elemento se coloca en el contenedor especial KeyValuePair. Se basa en CObject estándar y lo aumenta con punteros y datos adicionales. Más tarde describiremos la estructura de la clase del contenedor. Aparte de los punteros adicionales, el contenedor guarda la clave original del objeto y su hash.

El método AddObject() es un método de plantilla:

template<typename T>
bool CDictionary::AddObject(T key,CObject *value);

Esta entrada significa que el tipo del argumento key es substitutivo, y su tipo real se determina en la fase de compilación.

Por ejemplo, el método AddObject() puede ser activado en el código de la siguiente manera:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CObject* obj = new CObject();
   dictionary.AddObject(124,  obj);
   dictionary.AddObject("simple object",  obj);
   dictionary.AddObject(PERIOD_D1,  obj);
  }

Cualquiera de estas activaciones va a funcionar perfectamente debido a las plantillas.

El método DeleteObjectByKey() es contrario al método AddObject(). Este método elimina un objeto del diccionario por su clave.

Por ejemplo, Usted puede eliminar el objeto con la clave “Car” si existe:

if(dict.ContainsKey("Car"))
      dict.DeleteObjectByKey("Car");

Este código es muy parecido al código del método AddObject() por eso no vamos a detallarlo aquí.

Los métodos AddObject() y DeleteObjectByKey() no trabajan con los objetos directamente. En ves de eso, ellos colocan cada objeto en el contenedor-clase especial KeyValuePair que se basa en la clase estándar CObject. Este contenedor tiene punteros adicionales que permiten relacionar los elementos, también incluye la clave original y el hash calculado para el objeto. Además, el contenedor comprueba la clave pasada en cuanto a la igualdad, permitiendo evitar colisiones. Hablaremos sobre este mecanismo en el siguiente apartado dedicado al método ContainsKey().

Ahora simplemente mostraremos el contenido de esta clase:

//+------------------------------------------------------------------+
//| Contenedor para almacenar los elementos CObject                  |
//+------------------------------------------------------------------+
class KeyValuePair : public CObject
  {
private:
   string m_string_key;    // Guarda la clave string
   double m_double_key;    // Guarda la clave con punto flotante
   ulong  m_ulong_key;     // Guarda la clave entera sin signo
   ulong  m_hash;
public:
   CObject *object;
   KeyValuePair     *next_kvp;
   KeyValuePair     *prev_kvp;
   template<typename T>
   KeyValuePair(T key, ulong hash, CObject *obj);
   ~KeyValuePair();
   template<typename T>
   bool EqualKey(T key);
   ulong GetHash(){return m_hash;}
  };


template<typename T>
KeyValuePair::KeyValuePair(T key, ulong hash, CObject *obj)
{
   m_hash = hash;
   string name=typename(key);
   if(name=="string")
      m_string_key = (string)key;
   else if(name=="double" || name=="float")
      m_double_key = (double)key;
   else
      m_ulong_key = (ulong)key;
   object=obj;
}

KeyValuePair::~KeyValuePair()
{
   delete object;
}
template<typename T>
bool KeyValuePair::EqualKey(T key)
{
   string name=typename(key);
   if(name=="string")
      return key == m_string_key;
   if(name=="double" || name=="float")
      return m_double_key == (double)key;
   else
      return m_ulong_key == (ulong)key;
}

3.2. Identificación dinámica de los tipos a base de typename, cálculo del hash

Ahora, cuando ya hemos aprendido recibir los tipos indefinidos en nuestros métodos, tendremos que determinar su hash. Para todos los tipos de números enteros el hash será igual al valor de este tipo ampliado hasta el tipo ulong.

Para calcular el hash para los tipos double y float, hay que utilizar la conversión a través de las estructuras que ha sido descrita en el apartado “Conversión de tipos básicos a la clave única”. Para las cadenas de caracteres hay que usar la función hash. De cualquier forma, cada tipo de datos requiere su método para calcular el hash. Entonces, todo lo que necesitamos es determinar el tipo pasado y activar el método del cálculo del hash dependiendo de este tipo. Para eso vamos a necesitar una directiva especial typename.

El método que determina el hash a base de la clave se llama GetHashByKey().

Aquí tenemos su contenido:

//+-----------------------------------------------------------------------------+
//| Calcula el hash a base de la clave pasada. La clave puede representarse por |
//| cualquier tipo básico MQL.                                                  |
//+-----------------------------------------------------------------------------+
template<typename T>
ulong CDictionary::GetHashByKey(T key)
  {
   string name=typename(key);
   if(name=="string")
      return Adler32((string)key);
   if(name=="double" || name=="float")
     {
      dValue.value=(double)key;
      lValue=(ULongValue)dValue;
      ukey=lValue.value;
     }
   else
      ukey=(ulong)key;
   return ukey;
  }

Su lógica es muy simple. Usando la directiva typename, el método obtiene el nombre string del tipo transferido. Si la cadena se transfiere como la clave, typename devolverá el valor "string", si es el valor int, devolverá "int". Lo mismo pasará con cualquier otro tipo. De esta manera, todo lo que nos queda es comparar el valor devuelto con las constantes string necesarias, y si coincide con una de las constantes, llamar el manejador (handler) correspondiente.

Si la clave es el el tipo string, su hash se calcula por la función Adler32(). Si la clave está representada por tipos reales, se convierten en hash mediante la conversión de estructuras. Todos los demás tipos se convierten explícitamente en el tipo ulong que llegará a ser el hash.


3.3. Método ContainsKey(). Reacción a las colisiones del hash

El principal problema de cualquier función hash es la aparición de colisiones. Se trata de una situación cuando las claves diferentes dan el mismso hash. Si sus hashes coinciden, tendrá lugar la situación de ambigüedad: ambos objetos serán iguales desde el punto de vista de la lógica del diccionario. Para evitar esta situación, tenemos que comprobar el tipo de la clave real y solicitada, y devolver el valor positivo sólo si las claves reales coinciden. Precisamente así funciona el método ContainsKey(). Devuelve true si el objeto con la clave solicitada existe, en caso contrario devuelve false.

Tal vez es el método más útil y cómodo del diccionario. Permite averiguar si el objeto con una clave determinada existe:

#include <Dictionary.mqh>
CDictionary dict;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   if(dict.ContainsKey("Car"))
      printf("Car always exists.");
   else
      dict.AddObject("Car", new CCar());
  }

Por ejemplo, el código de arriba comprueba la presencia del objeto tipo CCar y añade CCar si no existe. Además, este método permite controlar la singularidad de la clave de cada nuevo objeto que se añade.

Si la clave del objeto ya se utiliza, el método AddObject() simplemente no va a añadir el nuevo objeto al diccionario.

template<typename T>
bool CDictionary::AddObject(T key,CObject *value)
  {
   if(ContainsKey(key))
      return false;
   ...
  }

Este método es tan universal que se utiliza de forma muy activa tanto por los usuarios, como por otros métodos de la clase.

Mostramos su contenido:

//+----------------------------------------------------------------------+
//| Comprueba si el diccionario contiene la clave del tipo arbitrario T. |
//| RETURNS:                                                             |
//|   Devuelve true si el objeto con esta clave ya existe,               |
//|   de lo contrario, devuelve false.                                   |
//+----------------------------------------------------------------------+
template<typename T>
bool CDictionary::ContainsKey(T key)
  {
   m_hash=GetHashByKey(key);
   m_index=GetIndexByHash(m_hash);
   if(CheckPointer(m_array[m_index])==POINTER_INVALID)
      return false;
   CList *list=m_array[m_index];
   m_current_kvp=list.GetCurrentNode();
   if(m_current_kvp == NULL)return false;
   if(m_current_kvp.EqualKey(key))
      return true;
   m_current_kvp=list.GetFirstNode();
   while(true)
     {
      if(m_current_kvp.EqualKey(key))
         return true;
      m_current_kvp=list.GetNextNode();
      if(m_current_kvp==NULL)
         return false;
     }
   return false;
  }

Primero, el método encuentra el hash del objeto usando GetHashByKey(). Luego, a base del hash obtiene el índice de la cadena CList en la que el objeto con este hash puede encontrarse. Si esta cadena no existe, entonces el objeto con esta clave tampoco existe. En este caso, termina su trabajo antes y devuelve false (el objeto con esta clave no existe). Si la cadena existe, se empieza su repaso.

Cada elemento de esta cadena está representado por un objeto del tipo KeyValuePair al que se le ofrece comparar la clave real transferida con la clave que guarda el elemento. Si las claves son iguales, el método devuelve true (el objeto con esta clave ya existe). El código que comprueba la igualdad de las claves se encuentra en el listado de la clase KeyValuePair.


3.4. Asignación dinámica y liberación de la memoria

El método Resize() es responsable de la asignación dinámica y liberación de la memoria en nuestro array asociativo. Este método se activa cada vez que el número de los elementos sea igual al tamaño del array m_array. Se activa también cuando los elementos se eliminan de la lista. Este método redefine los índices de almacenamiento para todos los elementos por eso funciona extremadamente lento.

Para evitar las activaciones frecuentes del método Resize(), cada vez el tamaño de la memoria asignada se duplica respecto al valor anterior. En otras palabras, para que el diccionario pueda contener 65 536 elementos, el método Resize() se activará sólo 16 veces (2^16). Después de veinte activaciones, el diccionario tendrá la capacidad para más de un millón de elementos (1 048 576). El método FindNextLevel() es responsable del crecimiento exponencial de elementos necesarios.

A continuación, se muestra el código del método Resize():

//+------------------------------------------------------------------+
//| Redimensiona el contenedor de almacenamiento de datos            |
//+------------------------------------------------------------------+
void CDictionary::Resize(void)
  {
   int level=FindNextLevel();
   int n=level;
   CList *temp_array[];
   ArrayCopy(temp_array,m_array);
   ArrayFree(m_array);
   m_array_size=ArrayResize(m_array,n);
   int total=ArraySize(temp_array);
   KeyValuePair *kv=NULL;
   for(int i=0; i<total; i++)
     {
      if(temp_array[i]==NULL)continue;
      CList *list=temp_array[i];
      int count=list.Total();
      list.FreeMode(false);
      kv=list.GetFirstNode();
      while(kv!=NULL)
        {
         int index=GetIndexByHash(kv.GetHash());
         if(CheckPointer(m_array[index])==POINTER_INVALID)
            m_array[index]=new CList();
         list.DetachCurrent();
         m_array[index].Add(kv);
         kv=list.GetCurrentNode();
        }
      delete list;
     }
   int size=ArraySize(temp_array);
   ArrayFree(temp_array);
  }

Este método funciona tanto para aumentar, como para reducir. Cuando el número de los elementos es mucho menor que el tamaño del array, se inicia el código de compresión de elementos en un array más compacto. Y al revés, cuando el tamaño del array ya no es suficiente, el array se redimensiona en la dirección del aumento de los elementos. Este código requiere muchos recursos computacionales.

Hay que redimensionar el array entero, pero antes hay que mover todos sus elementos en la copia temporal del array. Luego hace falta calcular nuevos índices para cada uno de los elementos, y sólo después de hacerlo, se puede colocar estos elementos en sus lugares nuevos.


3.5. Retoques finales: repaso de objetos e indexador conveniente

Bueno, nuestra clase CDictionary ya tiene los principales métodos para trabajar con ella. La presencia de un objeto con una clave determinada se averigua a través del método ContainsKey(). Podemos añadir un objeto al diccionario usando el método AddObject(). Además, podemos eliminar un objeto del diccionario usando el método DeleteObjectByKey().

Ahora nos queda hacer un repaso cómodo de todos los elementos del contenedor. Como ya sabemos, todos los elementos se almacenan ordenados según la clave. Pero sería más cómodo realizar el repaso de todos los elementos en el mismo orden en el que los hemos añadido a nuestro array asociativo. Para eso el contenedor KeyValuePair tiene dos punteros adicionales al siguiente y posterior elemento tipo KeyValuePair que han sido añadidos. Estos punteros nos permiten hacer el repaso de una manea consecutiva.

Por ejemplo, si hemos añadido los objetos a nuestro array asociativo de la siguiente manera:

CNumber --> CShip --> CWeather --> CHuman --> CExpert --> CCar

el repaso de estos elementos también va a ser consecutivo porque va a realizarse usando las referencias a KeyValuePair, como se muestra en la fig. 6:

Fig. 6. Esquema del repaso consecutivo del array

Fig. 6. Esquema del repaso consecutivo del array


Para este repaso se utilizan cinco método.

Mostramos su contenido y la descripción breve:

//+---------------------------------------------------------------------------+
//| Devuelve el objeto actual. Si el objeto no ha sido seleccionado, devuelve |
//| NULL                                                                      |
//+---------------------------------------------------------------------------+
CObject *CDictionary::GetCurrentNode(void)
  {
   if(m_current_kvp==NULL)
      return NULL;
   return m_current_kvp.object;
  }
//+----------------------------------------------------------------------------+
//| Devuelve el objeto anterior. Después de llamar al método, el objeto        |
//| actual se hace el anterior. Si el objeto no ha sido seleccionado, devuelve |
//| NULL.                                                                      |
//+----------------------------------------------------------------------------+
CObject *CDictionary:: GetPrevNode(void)
  {
   if(m_current_kvp==NULL)
      return NULL;
   if(m_current_kvp.prev_kvp==NULL)
      return NULL;
   KeyValuePair *kvp=m_current_kvp.prev_kvp;
   m_current_kvp=kvp;
   return kvp.object;
  }
//+-----------------------------------------------------------------------------+
//| Devuelve el siguiente objeto. Después de llamar al método, el objeto        |
//| actual se hace el siguiente. Si el objeto no ha sido seleccionado, devuelve |
//| NULL.                                                                       |
//+-----------------------------------------------------------------------------+
CObject *CDictionary::GetNextNode(void)
  {
   if(m_current_kvp==NULL)
      return NULL;
   if(m_current_kvp.next_kvp==NULL)
      return NULL;
   KeyValuePair *kvp=m_current_kvp.next_kvp;
   m_current_kvp=kvp;
   return kvp.object;
  }
//+----------------------------------------------------------------------------------+
//| Devuelve el primer nodo en la lista de nodos. Si en el diccionario no hay nodos, |
//| devuelve NULL.                                                                   |
//+----------------------------------------------------------------------------------+
CObject *CDictionary::GetFirstNode(void)
  {
   if(m_first_kvp==NULL)
      return NULL;
   m_current_kvp=m_first_kvp;
   return m_first_kvp.object;
  }
//+----------------------------------------------------------------------------------+
//| Devuelve el último nodo en la lista de nodos. Si en el diccionario no hay nodos, |
//| devuelve NULL.                                                                   |
//+----------------------------------------------------------------------------------+
CObject *CDictionary::GetLastNode(void)
  {
   if(m_last_kvp==NULL)
      return NULL;
   m_current_kvp=m_last_kvp;
   return m_last_kvp.object;
  }

Gracias a estos simples iteradores se puede realizar el recorrido de los elementos añadidos de una manera secuencial.

class CStringValue : public CObject
{
public:
   string Value;
   CStringValue();
   CStringValue(string value){Value = value;}
};
CDictionary dict;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   dict.AddObject("CNumber", new CStringValue("CNumber"));
   dict.AddObject("CShip", new CStringValue("CShip"));
   dict.AddObject("CWeather", new CStringValue("CWeather"));
   dict.AddObject("CHuman", new CStringValue("CHuman"));
   dict.AddObject("CExpert", new CStringValue("CExpert"));
   dict.AddObject("CCar", new CStringValue("CCar"));
   CStringValue* currString = dict.GetFirstNode();
   for(int i = 1; currString != NULL; i++)
   {
      printf((string)i + ":\t" + currString.Value);
      currString = dict.GetNextNode();
   }
  }

Este código imprimirá los valores string de los objetos en el mismo orden en el que han sido añadidos al diccionario:

2015.02.24 14:08:29.537 TestDict (USDCHF,H1) 6: CCar
2015.02.24 14:08:29.537 TestDict (USDCHF,H1) 5: CExpert
2015.02.24 14:08:29.537 TestDict (USDCHF,H1) 4: CHuman
2015.02.24 14:08:29.537 TestDict (USDCHF,H1) 3: CWeather
2015.02.24 14:08:29.537 TestDict (USDCHF,H1) 2: CShip
2015.02.24 14:08:29.537 TestDict (USDCHF,H1) 1: CNumber

Para mostrar todos los elementos al revés, sólo hay que cambiar dos filas del código en la función OnStart():

CStringValue* currString = dict.GetLastNode();
for(int i = 1; currString != NULL; i++)
 {
  printf((string)i + ":\t" + currString.Value);
  currString = dict.GetPrevNode();
 }

por consecuencia, los valores se muestran de forma inversa:

2015.02.24 14:11:01.021 TestDict (USDCHF,H1) 6: CNumber
2015.02.24 14:11:01.021 TestDict (USDCHF,H1) 5: CShip
2015.02.24 14:11:01.021 TestDict (USDCHF,H1) 4: CWeather
2015.02.24 14:11:01.021 TestDict (USDCHF,H1) 3: CHuman
2015.02.24 14:11:01.021 TestDict (USDCHF,H1) 2: CExpert
2015.02.24 14:11:01.021 TestDict (USDCHF,H1) 1: CCar


Capítulo 4. Análisis del rendimiento

4.1. Escribimos las prueba y evaluamos el rendimiento

La evaluación del rendimiento es un elemento importante en el diseño de una clase, especialmente si se trata de una clase para almacenar los datos. Es que en este caso, la clase puede ser utilizada por los programas muy diferentes. Sus algoritmos pueden ser muy exigentes en cuanto a la velocidad y requerir un alto rendimiento. Por eso es tan importante saber evaluar el rendimiento y buscar los “puntos débiles” de estos algoritmos.

Para empezar, vamos a escribir una simple prueba que mide la velocidad de adición y extracción de los elementos del diccionario. Escribiremos esta prueba como un script.

Su código viene a continuación:

//+------------------------------------------------------------------+
//|                                                    TestSpeed.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Dictionary.mqh>
#define BEGIN 50000
#define STEP  50000
#define END   1000000
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CDictionary dict(END+1);
   for(int j=BEGIN; j<=END; j+=STEP)
     {
      uint tiks_begin=GetTickCount();
      for(int i=0; i<j; i++)
         dict.AddObject(i,new CObject());
      uint tiks_add=GetTickCount()-tiks_begin;
      tiks_begin=GetTickCount();
      CObject *value=NULL;
      for(int i= 0; i<j; i++)
         value = dict.GetObjectByKey(i);
      uint tiks_get=GetTickCount()-tiks_begin;
      printf((string)j+" elements. Add: "+(string)tiks_add+"; Get: "+(string)tiks_get);
      dict.Clear();
     }
  }

Este código añade sucesivamente los elementos en el diccionario, y luego se dirige a ellos midiendo su velocidad. Luego cada elemento se elimina a través del método DeleteObjectByKey(). La función de sistema GetTickCount() mide la velocidad.

Usando este script hemos creado el gráfico que describe las dependencias de tiempo de estos tres métodos principales:

Fig. 7. Gráfico de puntos de dependencia entre el número de los elementos y el tiempo de trabajo de los métodos en milisegundos

Fig. 7. Gráfico de puntos de dependencia entre el número de los elementos y el tiempo de trabajo de los métodos en milisegundos

Como podemos ver, la mayor parte de tiempo se gasta en la colocación y eliminación de los elementos del diccionario. A pesar de eso, se esperaba que la eliminación de los elementos sería más rápida. Intentaremos mejorar el rendimiento de este método usando el perfilador del código. Este procedimiento se describe en el siguiente apartado.

Preste atención en el gráfico del método ContainsKey(). Es lineal. Eso significa que el acceso a un elemento aleatorio requiere aproximadamente la misma cantidad de tiempo independientemente del número de elementos en el array. Ésta es la propiedad obligatoria para un array asociativo verdadero.

Para ilustrar esta característica vamos a mostrar el diagrama de dependencia entre el tiempo medio de acceso a un elemento y el número de elementos en el array:

Fig.8. Tiempo medio de acceso a un elemento usando el método ContainsKey()

Fig. 8. Tiempo medio de acceso a un elemento usando el método ContainsKey()


4.2. Perfilaje del código. Velocidad contra el control automático de la memoria

El perfilaje del código es una técnica especial que permite medir el consumo de tiempo de todas las funciones. Para el perfilaje basta iniciar cualquier programa MQL5 pulsando el icono especial en MetaEditor.

Vamos a hacer lo mismo enviando nuestro script al proceso del perfilaje. Pasado un tiempo, nuestro script será ejecutado y tendremos el perfil temporal de todos los métodos que han sido ejecutados durante el funcionamiento del script. En la captura de abajo se ve que la mayor parte de tiempo se ha gastado en la ejecución de tres métodos: AddObject() (40% de tiempo), GetObjectByKey() (7% de tiempo) y DeleteObjectByKey() (53% de tiempo):

Fig. 9. Perfilaje del código en la función OnStart()

Fig. 9. Perfilaje del código en la función OnStart()

A su vez, más de 60% de la llamada a DeleteObjectByKey() se ha gastado en llamar al método Compress().

Pero este método es prácticamente vacío, la mayor parte de tiempo en él se gasta en la llamada al método Resize().

Aquí tenemos su contenido:

CDictionary::Compress(void)
{
   double koeff = m_array_size/(double)(m_total+1);
   if(koeff < 2.0 || m_total <= 4)return;
   Resize();
}

El problema está claro. Cuando se eliminan los elementos, de vez en cuando se inicia el método lento Resize() que reduce el tamaño del array, liberando dinámicamente la memoria.

Si queremos que la eliminación de los elementos sea más rápida, hay que desactivar este método. Por ejemplo, podemos introducir en la clase un método especial AutoFreeMemory() que establece la bandera de compresión. Estará por defecto, en este caso la compresión va a realizarse automáticamente. Pero si necesitamos la velocidad adicional, vamos a hacer la compresión manualmente en el momento oportuno. Para eso hagamos que el método Compress() sea público.

Si la bandera de compresión está desactivada, los elementos se eliminan tres veces más rápido:

Fig. 10. Tiempo de eliminación de elementos con el control manual de la memoria ocupada (comparación)

Fig. 10. Tiempo de eliminación de elementos con el control manual de la memoria ocupada (comparación)

El método Resize() se invoca no sólo para la compresión de datos, sino también cuando el número de elementos ya es demasiado grande y hace falta un array de mayor tamaño. En este caso no podemos evitar la llamada al método Resize(). Hay que llamarlo obligatoriamente para no crear las cadenas CList largas y lentas en el futuro. Sin embargo, podemos indicar el volumen necesario de nuestro diccionario con anticipación.

Por ejemplo, el número de los elementos a añadir se conoce de antemano. Sabiendo el número de elementos, el diccionario creará el array del tamaño necesario con antelación, y no será necesario redimensionar el array en el proceso de su relleno.

Con este fin, vamos a añadir otro constructor más que acepta la cantidad de elementos necesarios.

//+------------------------------------------------------------------+
//| Crea el diccionario con la capacidad capacity predefinida        |
//+------------------------------------------------------------------+
CDictionary::CDictionary(int capacity)
  {
   m_auto_free = true;
   Init(capacity);
  }

El método Init() ejecuta la operación principal:

//+------------------------------------------------------------------+
//| Realiza la inicialización del diccionario                        |
//+------------------------------------------------------------------+
void CDictionary::Init(int capacity)
  {
   m_free_mode=true;
   int n=FindNextSimpleNumber(capacity);
   m_array_size=ArrayResize(m_array,n);
   m_index = 0;
   m_hash = 0;
   m_total=0;
  }

Nuestras mejoras están hechas. Ha llegado la hora de medir el rendimiento del método AddObject() con el tamaño del array inicialmente especificado. Para eso vamos a cambiar dos primeras líneas en la función OnStart() de nuestro script:

void OnStart()
  {
//---
   CDictionary dict(END+1);
   dict.AutoFreeMemory(false);
   ...
  }

En este caso, END es un número muy grande igual a 1 000 000 de elementos.

Con las novedades introducidas, nuestro método AddObject() ha empezado a funcionar mucho más rápido:

Fig. 11. Tiempo de adición de elementos con el tamaño del array predefinido

Fig. 11. Tiempo de adición de elementos con el tamaño del array predefinido

A la hora de resolver los problemas reales de la programación, siempre tenemos que elegir entre el tamaño de la memoria utilizada y la velocidad de la ejecución. Si liberamos la memoria, en este proceso se tarda bastante tiempo. No obstante, en este caso habrá más tiempo disponible. Si no liberamos la memoria, la CPU no va a gastar tiempo para calcular las tareas complejas adicionales, pero habrá menos memoria libre. Utilizando los contenedores, la programación orientada a objetos permite distribuir los recursos entre la memoria y el tiempo de la CPU de una forma flexible, seleccionando su propia política de ejecución para cada tarea en particular.

En total, el rendimiento de los procesadores se escala mucho peor que el volumen de la memoria por eso, en la mayoría de los casos, se debe dar la preferencia a la velocidad de la ejecución, en vez del volumen de la memoria utilizada. Además, el control de la memoria es mucho más eficiente al nivel del usuario que en el modo automático porque habitualmente sabemos el estado final de la tarea.

Por ejemplo, en caso de nuestro script, sabemos de antemano que al final del repaso el número total de los elementos será igual a cero porque el método DeleteObjectByKey() los eliminará cuando se termine la tarea. Por esa razón no hace falta comprimir el array automáticamente. Será suficiente limpiarlo al final llamando al método Compress().

Ahora, cuando ya tenemos buenas métricas lineales de adición y eliminación de los elementos del array, vamos a analizar el tiempo medio de adición y eliminación en función del número total de elementos:

Fig.12. Tiempo medio de adición y eliminación de un elemento

Fig. 12. Tiempo medio de adición y eliminación de un elemento

Se ve que el tiempo medio de adición de un elemento en general es estacionario y no depende del número total de los elementos en el array. El tiempo medio de eliminación de un elemento se va un poco hacia arriba, lo que es indeseable. Sin embargo, no podemos decir exactamente si se trata de los resultados dentro de los límites del error o esto es una tendencia regular.


4.3. Rendimiento de CDictionary en comparación con CArrayObj estándar

Ha llegado el momento para la prueba más interesante: vamos a comparar CDictionary que hemos creado con la clase estándar del array CArrayObj. La clase CArrayObj no tiene los mecanismos para la distribución manual de la memoria con indicación del tamaño presupuesto en la fase de la creación, por eso vamos a desactivar el control manual de la memoria en CDictionary y nuestro mecanismo AutoFreeMemory() estará activado.

En el primer capítulo hemos conocido los puntos fuertes y débiles de cada clase. En CDictionary se puede añadir muy rápido un elemento a un lugar arbitrario, así como extraerlo rápidamente del diccionario. En cambio, en CArrayObj se puede realizar muy rápido el recorrido completo de los elementos. En CDictionary el recorrido será más lento porque se realiza moviéndose por punteros, y es una operación más lenta en comparación con la indexación directa.

Pues, vamos allá. Primero, vamos a crear la nueva versión de nuestra prueba:

//+------------------------------------------------------------------+
//|                                                    TestSpeed.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Dictionary.mqh>
#include <Arrays\ArrayObj.mqh>
#define TEST_ARRAY
#define BEGIN 50000
#define STEP  50000
#define END   1000000

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CDictionary dict(END+1);
   dict.AutoFreeMemory(false);
   CArrayObj objects;
   for(int j=BEGIN; j<=END; j+=STEP)
     {
      //---------- ADD --------------//
      uint tiks_begin=GetTickCount();
      for(int i=0; i<j; i++)
      {
         #ifndef TEST_ARRAY
            dict.AddObject(i,new CObject());
         #else
            objects.Add(new CObject());
         #endif 
      }
      uint tiks_add=GetTickCount()-tiks_begin;
      
      //---------- GET --------------//
      tiks_begin=GetTickCount();
      CObject *value=NULL;
      for(int i= 0; i<j; i++)
      {
         #ifndef TEST_ARRAY
            value = dict.GetObjectByKey(i);
         #else
            ulong hash = rand()*rand()*rand()*rand();
            value = objects.At((int)(hash%objects.Total()));
         #endif 
      }
      uint tiks_get=GetTickCount()-tiks_begin;
      
      //---------- FOR --------------//
      tiks_begin = GetTickCount();
      #ifndef TEST_ARRAY
         for(CObject* node = dict.GetFirstElement(); node != NULL; node = dict.GetNextNode());
      #else
         int total = objects.Total();
         CObject* node = NULL;
         for(int i = 0; i < total; i++)
            node = objects.At(i);
      #endif 
      uint tiks_for = GetTickCount() - tiks_begin;    
      
      //---------- DEL --------------//
      tiks_begin = GetTickCount();
      for(int i= 0; i<j; i++)
      {
         #ifndef TEST_ARRAY
            dict.DeleteObjectByKey(i);
         #else
            objects.Delete(objects.Total()-1);
         #endif
      }
      uint tiks_del = GetTickCount() - tiks_begin;
      
      //---------- SUMMARY --------------//
      printf((string)j+" elements. Add: "+(string)tiks_add+"; Get: "+(string)tiks_get + "; Del: "+(string)tiks_del + "; for: " + (string)tiks_for);
      #ifndef TEST_ARRAY
         dict.Clear();
      #else
         objects.Clear();
      #endif
     }
  }

Utiliza la macro TEST_ARRAY. Si está definido, la prueba ejecuta las operaciones con CArrayObj, si no, las ejecuta con CDictionary. CDictionary sale ganador en la prueba de adición de nuevos elementos.

Su modelo de redistribución de la memoria en este caso ha sido mejor:

Fig. 13. Tiempo gastado para añadir nuevos elementos a CArrayObj y CDictionary (comparación)

Fig. 13. Tiempo gastado para añadir nuevos elementos a CArrayObj y CDictionary (comparación)


Cabe mencionar que la causa del funcionamiento más lento es el modelo particular de la redistribución de la memoria en CArrayObj.

Se describe sólo con una línea del código fuente:

new_size=m_data_max+m_step_resize*(1+(size-Available())/m_step_resize);

Es el algoritmo lineal de la asignación de la memoria. Por defecto, se invoca después del relleno de cada 16 elementos. En CDictionary se utiliza el algoritmo exponencial: cada siguiente redistribución de la memoria asigna el doble de la memoria que la llamada anterior. Este gráfico muestra perfectamente el dilema entre el rendimiento y la memoria que se utiliza. El método Reserve() de la clase CArrayObj es más económico y requiere menos memoria pero funciona más lento. El método Resize() de la clase CDictionary funciona más rápido pero requiere más memoria, la mitad de la cual puede no usar en absoluto.

Vamos a realizar la última prueba. Vamos a calcular el tiempo del repaso completo de los contenedores CArrayObj y CDictionary. CArrayObj utiliza la indexación directa y CDictionary utiliza el paso por la referencia. CArrayObj permite dirigirse a cualquier elemento por su índice, en CDictionary es difícil hacerlo. El repaso consecutivo prácticamente no cede en nada ante la indexación directa:

Fig. 14. Tiempo gastado para el recorrido completo de CArrayObj y CDictionary

Fig. 14. Tiempo gastado para el recorrido completo de CArrayObj y CDictionary

Los desarrolladores del lenguaje MQL5 han hecho un gran trabajo y han optimizado los pasos por referencia. Estos pasos prácticamente no ceden ante la indexación directa en la velocidad, lo que se ve muy bien en el gráfico.

Es importante comprender que el repaso de los elementos tarda tan poco tiempo que se encuentra prácticamente en el límite de la posibilidad permitida de la función GetTickCount(). Es muy difícil usarla para medir el intervalo de tiempo de 15 milisegundos.


4.4. Recomendaciones generales al usar CArrayObj, CList y CDictionary

Vamos a confeccionar la tabla en la que describiremos las tareas principales que se encuentran en la programación, y las propiedades de los contenedores que hay que conocer para resolver dichas tareas.

Las propiedades con buen rendimiento durante la ejecución de las tareas tienen el color verde, con el color rojo se marcan las propiedades que no permiten resolver estas tareas de una manera eficaz.

Tareas CArrayObj CList CDictionary
Adición secuencial de nuevos elementos al final del contenedor. El contenedor tiene que asignar nueva memoria para el elemento. El traspaso de los elementos existentes a los nuevos índices no es necesaria. CArrayObj ejecutará esta operación rápidamente. El contenedor recuerda el primero, el actual y el último elemento. Gracias a eso, la adición del elemento nuevo al final de la lista (el último elemento recordado) será una operación muy rápida. El diccionario no tiene el concepto del “final” o “inicio”. La adición del elemento nuevo será muy rápida sin depender del número de los elementos de la colección.
Acceso al elemento arbitrario por su índice Gracias a la indexación directa, el acceso es muy rápido, tardando O(1). El acceso a cada elemento se realiza a través del recorrido de todos los elementos anteriores. Esta operación lleva O(n) de tiempo. El acceso a cada elemento se realiza a través del recorrido de todos los elementos anteriores. Esta operación lleva O(n) de tiempo.
Acceso al elemento arbitrario por su clave. No disponible No disponible El cálculo del hash e índice por la clave es una operación rápida y eficaz. La eficacia de las funciones hash tiene mucha importancia para las claves string. Esta operación tarda O(1).
Adición/eliminación de elementos nuevos por el índice arbitrario. CArrayObj no sólo necesitará asignar la memoria para elemento nuevo, sino también mover a otro lugar todos los elementos cuyo índice supera el índice del elemento insertado. Por eso en CArrayObj es una operación lenta.

La adición o eliminación del elemento en CList es una operación muy rápida, pero el acceso al índice necesario se realiza durante O(n). Es una operación lenta.

En CDictionary el acceso al elemento por el índice se realiza a base de la lista CList y requiere mucho tiempo. No obstante, la eliminación y la inserción del elemento es una operación rápida.
Adición/eliminación de elementos nuevos por la clave arbitrario. No disponible No disponible Debido al hecho de que cada elemento nuevo se inserta en la lista CList, la adición y la eliminación de elementos nuevos se realiza rápidamente porque no hace falta redimensionar el array con cada inserción/eliminación.
Uso y control de la memoria. Hace falta el redimensionamiento del array. Es una operación que consume muchos recursos y que requiere o mucho tiempo o mucha memoria.  El control de la memoria no se usa. Cada elemento ocupa tanta memoria cuanta necesita. Hace falta el redimensionamiento del array. Es una operación que consume muchos recursos y que requiere o mucho tiempo o mucha memoria.
Repaso de los elementos. Las operaciones que hay que ejecutar para cada elemento del vector. Gracias a la indexación directa de los elementos, se realiza muy rápido. Pero en algunas ocaciones, es importante realizar el recorrido en dirección inversa (por ejemplo, cuando se elimina secuencialmente el último elemento). Puesto que el repaso hay que realizar sólo una vez, la referencia directa es una operación rápida. Puesto que el repaso hay que realizar sólo una vez, la referencia directa es una operación rápida.

Capítulo 5. Documentación para la clase CDictionary

5.1. Métodos principales para añadir, eliminar y acceder a los elementos

5.1.1. Método AddObject()

Añade en el elemento nuevo tipo CObject con la clave T key. Como la clave se puede utilizar cualquier tipo básico.

template<typename T>
bool AddObject(T key,CObject *value);

Parámetros

  • [in] key – clave única representada por uno de los tipos básicos (string, ulong, char, enum, etc.).
  • [in] value - objeto con CObject como la clase básica.

Valor devuelto

Devuelve true si el objeto ha sido añadido al contenedor, en caso contrario devuelve false. Si la clave del objeto a añadir ya existe en el contenedor, el método devolverá el valor negativo y el objeto no será añadido.


5.1.2. Método ContainsKey()

Comprueba si existe en el contenedor el objeto cuya clave es igual a T key. Como la clave se puede utilizar cualquier tipo básico.

template<typename T>
bool ContainsKey(T key);

Parámetros

  • [in] key – clave única representada por uno de los tipos básicos (string, ulong, char, enum, etc.).
Valor devuelto

Devuelve true si el objeto con la clave a comprobar se encuentra en el contenedor. De lo contrario, devuelve false.


5.1.3. Método DeleteObjectByKey()

Elimina el objeto por la clave T key predefinida. Como la clave se puede utilizar cualquier tipo básico.

template<typename T>
bool DeleteObjectByKey(T key);

Parámetros

  • [in] key  – clave única representada por uno de los tipos básicos (string, ulong, char, enum, etc.).

Valor devuelto

Devuelve true si el objeto con la clave predefinida ha sido eliminado. Si el objeto con la clave predefinida no existe o no se ha podido eliminarla, devuelve false.


5.1.4. Método GetObjectByKey()

Devuelve el objeto por la clave T key predefinida. Como la clave se puede utilizar cualquier tipo básico.

template<typename T>
CObject* GetObjectByKey(T key);

Parámetros

  • [in] key  – clave única representada por uno de los tipos básicos (string, ulong, char, enum, etc.).

Valor devuelto

Devuelve el objeto correspondiente a la clave predefinida. Si el objeto con la clave predefinida no existe, devuelve NULL.


5.2. Métodos para controlar la memoria

5.2.1. Constructor CDictionary()

El constructor básico crea un objeto CDictionary con el tamaño inicial del array básico igual a 3 elementos.

El array se aumenta o se comprime automáticamente cuando el diccionario se llena o sus elementos se eliminan.

CDictionary();

5.2.2. Constructor CDictionary(int capacity)

El constructor crea un objeto CDictionary con el tamaño inicial del array básico igual a 'capacity' elementos.

El array se aumenta o se comprime automáticamente cuando el diccionario se llena o sus elementos se eliminan.

CDictionary(int capacity);

Parámetros

  • [in] capacity  – número de elementos iniciales.

Nota

El establecimiento del tamaño límite del array inmediatamente después de su inicialización permite evitar las llamadas al método interno Resize(), y por tanto aumenta el rendimiento al insertar nuevos elementos.

Sin embargo, hay que tener en cuenta que cuando se eliminan los elementos estando activada la bandera del control automático de la memoria (AutoMemoryFree(true)), el contenedor se comprimirá automáticamente a pesar del ajuste capacity. Para evitar la compresión anticipada, ejecute la primera operación de eliminación de los objetos después del llenado completo de los contenedores, o si eso no es posible, desactive el control automático de la memoria (AutoMemoryFree(false)).


5.2.3. Método FreeMode(bool mode)

Establece el modo de eliminación de los objetos que se encuentran en el contenedor.

Si el modo de eliminación está activado, después de la eliminación del objeto del contenedor, también estará sujeto al procedimiento “delete”. Se invoca su destructor y ya no estará disponible. Si el modo de eliminación no está activado, el destructor no se invoca para el objeto, y él sigue estando disponible desde otros puntos del programa.

void FreeMode(bool free_mode);

Parámetros

  • [in] free_mode  – bandera del modo de eliminación de objetos.


5.2.4. Método FreeMode()

Devuelve la bandera de eliminación de los objetos que se encuentran en el contenedor (ver FreeMode(bool mode)).

bool FreeMode(void);

Valor devuelto

Devuelve true si el objeto se elimina por el operador delete tras su eliminación del contenedor, en caso contrario devuelve false.


5.2.5. Método AutoFreeMemory(bool autoFree)

Establece la bandera del control automático de la memoria. El control automático de la memoria está activado por defecto. En este caso, la compresión del array (redimensionamiento desde el mayor tamaño hacia el menor) se hace automáticamente. Si está desactivada, la compresión no se ejecuta. Eso puede acelerar considerablemente la ejecución del programa ya que la llamada al método de mucho consumo de recursos Resize() no será necesaria. Pero en este caso Usted mismo tendrá que monitorear el tamaño del array y realizar su compresión manualmente llamando al método Compress().

void AutoFreeMemory(bool auto_free);

Parámetros

  • [in] auto_free  – bandera del modo de control de la memoria.

Valor devuelto

Devuelve true si hace falta activar el control de la memoria y llamar al método Compress() automáticamente, de lo contrario devuelve false.


5.2.6. Método AutoFreeMemory()

Devuelve la bandera que indica si se utiliza el modo automático del control de la memoria (ver método FreeMode(bool free_mode)).

bool AutoFreeMemory(void);

Valor devuelto

Devuelve true si se utiliza el modo del control de la memoria, de lo contrario, false.


5.2.7. Método Compress()

Comprime el diccionario y redimensiona los elementos La compresión se realiza sólo si es posible.

Hay que llamar a este método sólo si el modo del control automático de la memoria está desactivado.

void Compress(void);

5.2.8. Método Clear()

Elimina todos los elementos que se encuentran en el diccionario. Si la bandera del control de la memoria está activada, para cada elemento eliminado también se llama el destructor.

void Clear(void);

5.3. Métodos para recorrer los elementos en el diccionario

Todos los elementos del diccionario están interconectados. Cada nuevo elemento agregado se interconecta con el anterior. De esta manera, se hace posible el repaso consecutivo de todos los elementos en el diccionario. En este caso, se realiza la referencia por la lista enlazada. Los métodos de este apartado ayudan a realizar el repaso haciendo que la iteración del diccionario sea más cómoda.

Podemos utilizar la siguiente construcción como repaso del diccionario:

for(CObject* node = dict.GetFirstNode(); node != NULL; node = dict.GetNextNode())
  {
   // Aquí node es el elemento actual del diccionario.
  }

5.3.1. Método GetFirstNode()

Devuelve el primer elemento añadido al diccionario.

CObject* GetFirstNode(void);

Valor devuelto

Devuelve el puntero al primer objeto CObject que ha sido añadido al diccionario.


5.3.2. Método GetLastNode()

Devuelve el último elemento añadido al diccionario.

CObject* GetLastNode(void);

Valor devuelto

Devuelve el puntero al último objeto CObject que ha sido añadido al diccionario.


5.3.3. Método GetCurrentNode()

Devuelve el elemento seleccionado actual. El elemento tiene que ser previamente seleccionado por uno de los métodos para el repaso de los elementos en el diccionario o por el método de acceso al elemento del diccionario (ContainsKey(), GetObjectByKey()).

CObject* GetLastNode(void);

Valor devuelto

Devuelve el puntero al último objeto CObject que ha sido añadido al diccionario.


5.3.4. Método GetNextNode()

Devuelve el elemento el que sigue al actual. Si el elemento actual es el último o no ha sido seleccionado, devuelve NULL.

CObject* GetLastNode(void);

Valor devuelto

Devuelve el puntero al objeto del tipo CObject que sigue al actual. Si el elemento actual es el último o no ha sido seleccionado, devuelve NULL.


5.3.5. Método GetPrevNode()

Devuelve el elemento que precede al elemento actual. Si el elemento actual es el primero o no ha sido seleccionado, devuelve NULL.

CObject* GetPrevNode(void);

Valor devuelto

Devuelve el puntero al objeto del tipo CObject que precede al elemento actual. Si el elemento actual es el primero o no ha sido seleccionado, devuelve NULL.


Conclusión

Hemos considerado las principales propiedades de los contenedores de datos. Cada contenedor de datos debe utilizarse cuando sus posibilidades permiten resolver el problema planteado con mayor eficacia.

Es mejor usar los arrays regulares en las ocasiones cuando es suficiente tener el acceso consecutivo al elemento. Los diccionarios y los arrays asociativos convienen mejor para los casos cuando es preferible acceder al elemento por su clave única. Las listas convienen mejor para los casos de las reposiciones frecuentes de los elementos: eliminación, inserción, adición de nuevos elementos.

El diccionario permite resolver los problemas algorítmicas interesantes. Donde otros contenedores de datos requieren las funciones adicionales para su procesamiento, el diccionario permite organizar el acceso a los elementos existentes de forma máximamente eficaz. Por ejemplo, a base del diccionario se puede construir un modelo de eventos donde cada evento del tipo OnChartEvent() va a entregarse directamente a la clase que controla uno u otro elemento gráfico. Para eso en el diccionario sólo hay que asociar cada instancia de la clase con el nombre del objeto controlado por la clase.

Puesto que el diccionario es un algoritmo universal, se puede aplicarlo en las áreas más diversas. Al leer este artículo, queda entendido que su funcionamiento se basa en los algoritmos bastante sencillos y seguros al mismo tiempo, haciendo que el trabajo con él sea cómodo y eficaz.


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/1334

Archivos adjuntos |
dictionary.mqh (37.85 KB)
Líneas de tendencia basadas en los fractales usando MQL4 y MQL5 Líneas de tendencia basadas en los fractales usando MQL4 y MQL5
En este artículo se describe la solución de automatización del proceso de la construcción de las líneas de tendencia a base del indicador Fractals usando MQL4 y MQL5. La estructura del artículo está representada como la comparación en el marco de la solución del problema planteado desde las posiciones de dos lenguajes. La construcción de las líneas de tendencia se realiza usando dos últimos fractales conocidos.
Neuroredes gratis y a mogollón: NeuroPro y MetaTrader 5 Neuroredes gratis y a mogollón: NeuroPro y MetaTrader 5
Si los programas especializados de nueroredes para el trading le parecen caros o complicados (o al contrario, primitivos), entonces pruebe NeuroPro, está en ruso, es gratuito y contiene el conjunto ideal de posibilidades para los aficionados. Prodrá familiarizarse con su uso en MetaTrader 5 en este artículo.
Crear aplicación interactiva para visualizar los canales RSS en MetaTrader 5 Crear aplicación interactiva para visualizar los canales RSS en MetaTrader 5
En este artículo se describe cómo crear la aplicación que visualiza los canales RSS. Además, hablaremos sobre los aspectos del uso de la Biblioteca estándar durante la creación de los programas interactivos en MetaTrader 5.
Trading bidireccional y cobertura (hedging) de posiciones en MetaTrader 5 usando API HedgeTerminal, Parte 2 Trading bidireccional y cobertura (hedging) de posiciones en MetaTrader 5 usando API HedgeTerminal, Parte 2
En este artículo se describe el nuevo enfoque en las cuestiones de la cobertura (hedging) de posiciones y se pone punto en las discusiones entre los usuarios de MetaTrader 4 y MetaTrader 5 sobre esta materia. Es la continuación de la primera parte: “Trading bidireccional y cobertura (hedging) de posiciones en MetaTrader 5 usando el panel HedgeTerminal, Parte 1”. En la segunda parte se describe la integración de los EAs personalizados con HedgeTerminalAPI, una biblioteca especial de virtualización que permite tradear bidireccionalmente estando en un entorno cómodo que permite gestionar sus posiciones de una manera sencilla y clara.