English Русский 中文 Deutsch 日本語 Português
preview
Aprendiendo MQL5 de principiante a profesional (Parte III): Tipos de datos complejos y archivos de inclusión

Aprendiendo MQL5 de principiante a profesional (Parte III): Tipos de datos complejos y archivos de inclusión

MetaTrader 5Ejemplos | 28 noviembre 2024, 16:03
435 0
Oleh Fedorov
Oleh Fedorov

Introducción

Este artículo es la continuación de la serie para principiantes. Damos por sentado que el lector ya conoce el material de los dos artículos anteriores.

El primer artículo supone una introducción. Asume que el lector no ha tenido ningún contacto con la programación, y ofrece una comprensión esencial de las herramientas que necesita un programador para su trabajo, describe los tipos básicos de programas e introduce algunos conceptos básicos, en particular el concepto de "función".

El segundo artículo describe cómo trabajar con datos, e introduce los conceptos "literal", "variable", "tipo de datos", "operador", etc., abarcando asimismo los operadores básicos de modificación de datos: aritmético, lógico, a nivel de bits...

En este artículo, describiremos cómo puede crear un programador tipos de datos complejos, tales como:

  • estructuras;
  • uniones;
  • clases (a nivel inicial);
  • tipos que permiten utilizar el nombre de la variable como una función. Esto nos permitirá, entre otras cosas, transmitir funciones como parámetros a otras funciones.

El artículo también explicará cómo conectar archivos de texto externos utilizando la directiva del preprocesador #include, para dotar a nuestro programa de modularidad y flexibilidad. Permítame recordarle que los datos pueden organizarse de diferentes maneras, pero el compilador siempre necesitará saber cuánta memoria requerirá nuestro programa, por lo que antes de utilizar los datos, deberemos describirlos especificando su tipo.

Los tipos de datos simples como double, enum, string y otros ya los describimos en el segundo artículo. En este, analizamos con detalle tanto las variables (datos que cambian a medida que se trabaja) como las constantes. Sin embargo, en programación suelen darse situaciones en las que resulta más conveniente componer tipos más complejos a partir de datos sencillos. Hablaremos de estos "constructos" en la primera parte de este artículo.

Cuanto más modular sea un programa, más fácil resultará desarrollarlo y mantenerlo. Esto es especialmente importante al trabajar en equipo. No obstante, para los "solitarios" resulta mucho más fácil buscar errores no en una pieza "sólida" de código, sino en pequeños fragmentos del mismo. Sobre todo si regresa al código después de mucho tiempo para añadir algunas funciones a su programa o corregir algunos errores lógicos que no se apreciaron en su momento.

Si implementamos estructuras adecuadas para los datos, asignamos funciones cómodas en lugar de un flujo "sólido" de condiciones y ciclos, y distribuimos diferentes bloques de código relacionados lógicamente en distintos archivos, realizar los cambios será mucho más fácil.


Estructuras

Las estructuras describen un conjunto complejo de datos que se almacena cómodamente en una única variable. Por ejemplo, la información sobre la hora de una transacción intradía debe contener minutos, segundos y horas.

Obviamente, podríamos crear tres variables para cada componente y hacer referencia a cada una de ellas según sea necesario. Sin embargo, como estos datos forman parte de la misma descripción y la mayoría de las veces se utilizan juntos, resultará más cómodo describir un tipo distinto para dichos datos. Al mismo tiempo, la estructura puede contener datos adicionales de otros tipos, como la zona horaria o cualquier otra cosa que necesitemos.

En el caso más sencillo, la estructura se describe del siguiente modo:

struct IntradayTime {
  int hours;
  int minutes;
  int seconds;
  string timeCodeString;
};  // note the semicolon after the curly brace

Ejemplo 1. Ejemplo de estructura para describir la hora de una transacción.

Con este código, crearemos un nuevo tipo de datos IntradayTime. Entre los paréntesis de esta descripción se enumerarán todas las variables que queremos combinar. Como consecuencia, todas las variables de tipo IntradayTime contendrán horas, minutos y segundos.

A cada parte de la estructura dentro de cada variable se accederá mediante el signo "." (punto).

IntradayTime dealEnterTime;

dealEnterTime.hours = 8;
dealEnterTime.minutes = 15;
dealEnterTime.timeCodeString = "GMT+2";

Ejemplo 2. Uso de variables de tipo estructura.

Al describir una estructura, sus variables "internas" (más a menudo llamadas "campos") pueden tener cualquier tipo de datos válido, incluso se pueden usar otras estructuras. Por ejemplo, así:

// Nested structure
struct TradeParameters
{
   double stopLoss;
   double takeProfit;
   int magicNumber;
};

// Main structure
struct TradeSignal
{
   string          symbol;    // Symbol name
   ENUM_ORDER_TYPE orderType; // Order type (BUY or SELL)
   double          volume;    // Order volume
   TradeParameters params;    // Nested structure as parameter type
};

// Using the structure
void OnStart()
{

// Variable description for the structure
   TradeSignal signal;

// Initializing structure fields
   signal.symbol = Symbol();
   signal.orderType = ORDER_TYPE_BUY;
   signal.volume = 0.1;

   signal.params.stopLoss = 20;
   signal.params.takeProfit = 40;
   signal.params.magicNumber = 12345;

// Using data in an expression
   Print("Symbol: ",  signal.symbol);
   Print("Order type: ",  signal.orderType);
   Print("Volume: ",  signal.volume);
   Print("Stop Loss: ",  signal.params.stopLoss);
   Print("Take Profit: ",  signal.params.takeProfit);
   Print("Magic Number: ",  signal.params.magicNumber);
}

Ejemplo 3. Uso de una estructura para describir el tipo de campos de otra estructura.


Si se usan constantes en lugar de expresiones como valores iniciales de la estructura, podremos utilizar la entrada abreviada para la inicialización. Para ello se usan paréntesis. Por ejemplo, el bloque de inicialización del ejemplo anterior podría reescribirse como vemos:

TradeSignal signal = 
  {
    "EURUSD", 
    ORDER_TYPE_BUY, 
    0.1, 
 
     {20.0,  40.0,  12345}
  };

Ejemplo 4. Inicialización de una estructura usando constantes.


El orden de las constantes deberá coincidir con el de los campos de la descripción. También podemos inicializar solo una parte de la estructura enumerando los valores de los campos iniciales. En este caso, todos los demás campos se inicializarán con ceros.

Existen muchas estructuras predefinidas en MQL5, por ejemplo, MqlDateTime, MqlTradeRequest, MqlTick y otras. En general, su uso no resulta más complicado que el descrito en esta sección. El lector podrá encontrar la lista de campos de estas y otras muchas estructuras descritas con todo detalle en la ayuda.

Además, esta lista para cualquier estructura (y otros tipos complejos) se será mostrada por el MetaEditor si creamos una variable del tipo requerido y, a continuación, escribimos su nombre y pulsamos un punto (".") en el teclado.

Lista de campos de estructura

Figura 1. Lista de campos de estructura en el MetaEditor.

Todos los campos de la estructura están disponibles por defecto para todas las funciones de nuestro programa.


Ahora, para aquellos que saben trabajar con dlls "externas", diremos unas palabras sobre las estructuras MQL5

Advertencia. Esta sección puede resultar difícil para los principiantes, por lo que cuando lea este artículo por primera vez, puede saltársela e ir directamente a las uniones y volver a esta sección más tarde.

Por defecto, los datos de las estructuras MQL5 se ubican "empaquetados", es decir, directamente unos detrás de otros, por lo que si queremos que la estructura ocupe un determinado número de bytes, puede que tengamos que añadir elementos de relleno adicionales.

Le recuerdo que en este caso será mejor colocar primero los datos de mayor tamaño y después los de menor. Así se evitarán muchos problemas. No obstante, las estructuras MQL5 también tienen la capacidad de "alinear" los datos utilizando un operador pack especial:

struct pack(sizeof(long)) MyStruct1
     {
      // structure members will be aligned on an 8-byte boundary
     };

// or

struct MyStruct2 pack(sizeof(long))
     {
      // structure members will be aligned on an 8-byte boundary
     };

Ejemplo 5. Alineación estructural.

Entre paréntesis en pack solo pueden encontrarse los números 1, 2, 4, 8, 16.

El comando especial offsetof nos permitirá obtener el desplazamiento en bytes de cualquier campo de la estructura con respecto al principio. Por ejemplo, si tomamos la estructura TradeParameters del ejemplo 3, podremos utilizar el siguiente código para obtener el desplazamiento del campo stopLoss:

Print (offsetof(TradeParameters, stopLoss)); // Result: 0

Ejemplo 6. Uso del operador offsetof.

Las estructuras que NO contienen cadenas, arrays dinámicos, objetos basados en clases ni punteros se denominan simples. Las variables de estructuras simples, así como los arrays formados por tales elementos, pueden transmitirse libremente a funciones importadas de bibliotecas dll externas.

La copia de estructuras simples entre sí usando el operador de asignación está permitida, pero solo en dos casos:

  • si las variables pertenecen al mismo tipo;
  • o si los tipos de variables están conectados por una línea directa de herencia.

    Esto significa que si tenemos definidas las estructuras "plantas" y "árboles", cualquier variable "plantas" podrá copiarse en cualquier variable "árboles", y viceversa. Sin embargo, si también tenemos "arbustos", entonces de "arbustos" a "árboles" (o viceversa), solo se podrá elemento por elemento.

En todos los demás casos, incluso las estructuras con campos idénticos deberán copiarse elemento por elemento.

Las mismas reglas se aplicarán a la conversión de tipos: no podremos convertir directamente "arbusto" en "árbol", aunque ambos tengan los mismos campos, pero sí podremos convertir "planta" en "arbusto"....

No obstante, si realmente necesitamos convertir un tipo "arbusto" a un tipo "árbol", podremos utilizar uniones. Lo principal es considerar las limitaciones de las uniones descritas en la sección correspondiente de este artículo. Y así, cualquier campo numérico se convertirá sin problemas.

//---
enum ENUM_LEAVES
  {
   rounded,
   oblong,
   pinnate
  };

//---
struct Tree
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
struct Bush
  {
   int               trunks;
   ENUM_LEAVES       leaves;
  };

//---
union Plant
  {
   Bush bush;
   Tree tree;
  };

//---
void OnStart()
  {
   Tree tree = {1, rounded};
   Bush bush;
   Plant plant;

// bush = tree; // Error!
// bush = (Bush) tree; // Error!
   plant.tree = tree;
   bush = plant.bush; // No problem...

   Print(EnumToString(bush.leaves));
  }
//+------------------------------------------------------------------+

Ejemplo 7. Conversión de estructuras usando uniones.

Bueno, creo que nos detendremos aquí. Una descripción completa de todas las características de las estructuras contiene un poco más de detalles y matices de los que se describen en este artículo. Si necesita comparar las estructuras de MQL5 con otros lenguajes o aprender algunas sutilezas... Espero que ya sepa dónde buscar.

Pero para los principiantes, creo que el material escrito sobre estructuras resultará suficiente, así que pasaremos a la siguiente sección.


Uniones

Para algunas tareas puede ser necesario interpretar datos en la misma celda de memoria como variables de diferentes tipos. Este tipo de tareas son las más frecuentes a la hora de convertir tipos de estructuras. Estas tareas también pueden producirse en el cifrado.

La descripción de estos datos casi no se distingue de la descripción de estructuras simples:

// Creating a type
union AnyNumber {
  long   integerSigned;  // Any valid data types (see further)
  ulong  integerUnsigned;
  double doubleValue;
};

// Using
AnyNumber myVariable;

myVariable.integerSigned = -345;

Print(myVariable.integerUnsigned);
Print(myVariable.doubleValue);

Ejemplo 8. Uso de uniones.

Para evitar errores, recomendamos utilizar datos cuyo tipo ocupe el mismo espacio de memoria en las uniones (aunque en algunas transformaciones esto sea innecesario o incluso perjudicial).

Los siguientes tipos de datos NO pueden ser miembros de una unión:

  • arrays dinámicos,
  • cadenas,
  • punteros a objetos y funciones,
  • objetos de clase,
  • objetos de estructuras que tienen constructores o destructores,
  • objetos de estructuras que tengan los elementos de los puntos 1-5.

No hay más restricciones.

Pero una vez más, si su estructura usa algún campo de cadena, el compilador generará un error. Tenga esto en cuenta.


Concepto inicial de POO

El enfoque orientado a objetos (POO) es muy común en la programación actual. La esencia de este enfoque reside en que todo lo que ocurre en el programa se divide en bloques separados. Cada bloque describe una "entidad" distinta: un archivo, una línea, una ventana, una lista de precios...

El objetivo de cada bloque consiste en garantizar que los datos y las acciones necesarias para procesarlos se recojan en un único lugar. Si ese "montaje" se hace correctamente, se obtendrán muchos beneficios:

  • permite reutilizar el código muchas veces;
  • facilita al IDE la sustitución rápida de los nombres de variables y funciones relacionadas con objetos concretos;
  • facilita la detección de errores y reduce la posibilidad de que se produzcan errores nuevos;
  • facilita que distintas personas (o incluso equipos) trabajen en distintas partes del código en paralelo;
  • facilita la modificación del código, aunque haya pasado mucho tiempo;
  • y todo ello hace posible, en última instancia, una escritura más rápida de los programas, una mayor fiabilidad y una codificación más sencilla al final.

Y esta disposición es generalmente natural, ya que usa los principios del pensamiento en la vida ordinaria. Nosotros clasificamos todo tipo de objetos todo el tiempo: "Esto de aquí pertenece a la clase animal, esto son plantas, esto son muebles...". El mobiliario, a su vez, se presenta como armarios y tapizados.... Y así sucesivamente.

Todas estas clasificaciones usan determinadas características de los objetos, sus descripciones. Digamos que las plantas tienen tronco y raíces, y los animales extremidades móviles sobre las que se desplazan. En general, existen algunos atributos propios de cada clase. En programación ocurre exactamente lo mismo.

Si quiere crear una biblioteca para trabajar con rectas, necesitará entender qué puede hacer cada recta y qué tiene disponible para ello . Por ejemplo, cualquier línea recta tiene un punto inicial, un punto final, un grosor y un color.

Son sus propiedades, o atributos, o campos de clase de línea recta. Para las acciones, podemos utilizar verbos como "dibujar", "desplazar", "copiar con un desplazamiento determinado", "girar con cierto ángulo"...

Si un objeto directo es capaz de realizar todas estas acciones por sí mismo, los programadores hablarán de los métodos de ese objeto.

Las propiedades y los métodos juntos se denominarán miembros (elementos) de la clase.

Resulta que, para crear una recta con este método, primero tendremos que crear una clase (descripción) de esta recta -y de todas las demás rectas del programa- y, a continuación, bastará con decírselo al compilador: "Esta y esta variable son directas, mientras que esta función las utiliza...".

Una clase es un tipo de variable que contiene una descripción de las propiedades y métodos de los objetos pertenecientes a esa clase.

Y la forma en que se describe la clase resulta muy similar a la estructura. La principal diferencia es que, por defecto, todos los miembros de una clase solo estarán disponibles dentro de esa clase. La estructura tiene a todos sus miembros a disposición de todas las funciones de nuestro programa. Vamos a describir el esquema general de creación de la clase que necesitamos:

// class (variable type) description
class TestClass { // Create a type

private:          // Describe private variables and functions 
                  //   They will only be accessible to functions within the class 
  
// Description of data (class "properties" or "fields")
  double m_privateField; 

// Description of functions (class "methods")
  bool  Private_Method(){return false;} 

public:           // Description of public variables and functions 
                  //   They will be available to all functions that use objects of this class    

// Description of data (class "properties", "fields", or "members")   
  int m_publicField; 

// Description of functions (class "methods")   
  void Public_Method(void)
    {
     Print("Value of `testElement` is ",  testElement );   
    }
 }; 


Ejemplo 9. Descripción de la estructura de clase

Las palabras clave public: y private: definirán el ámbito de los miembros de la clase.

Todo lo que se encuentre por debajo de la palabra public: estará disponible "fuera" de la clase, es decir, para otras funciones de nuestro programa, incluso las que no pertenezcan a esta clase.

Todo lo que se encuentre por encima de esta sección (y por debajo de la palabra private:) estará "oculto": solo las funciones de la misma clase tendrán acceso a estos elementos.

Una clase podrá contener tantas secciones public: y private: como deseemos.

Sin embargo, a pesar de la sugerencia del recuadro, resultará mejor utilizar solo un bloque por ámbito (uno private: y uno public:) para que todos los datos o funciones con el mismo acceso estén uno al lado del otro. Sin embargo, algunos desarrolladores experimentados siguen prefiriendo crear cuatro secciones: dos (privada y pública) para las funciones y dos para las variables. Eso dependerá de cada uno.

En principio, la palabra private: puede omitirse, ya que todos los miembros de la clase que no se describan como public: serán privados por defecto (a diferencia de las estructuras). Pero no recomendamos hacer esto porque sería incómodo leer tal código.

Debemos recordar que, en general, al menos una función de la clase descrita debe ser "pública", de lo contrario la clase será inútil en la mayoría de los casos. Hay excepciones, pero son raras.

Se considera una buena práctica de programación, en la sección pública, colocar solo funciones (NO variables) para proteger los datos. Esto permitirá modificar variables de clase solo utilizando métodos de esa clase. Este enfoque aumentará la fiabilidad del código del programa.

Una vez descrita una clase, bastará con crear variables del tipo adecuado para utilizarla en el lugar correcto del programa. La creación de variables se realizará como de costumbre. A los métodos y propiedades de cada variable de este tipo se suele acceder mediante el símbolo de punto, igual que en las estructuras:
// Description of the variable of the required type
TestClass myTestClassVariable;

// Using the capabilities of this variable
myTestClassVariable.testElement = 5;
myTestClassVariable.PrintTestElement();

Ejemplo 10. Uso de la clase.

Para ilustrar cómo funcionan las propiedades públicas y privadas, intente pegar el código del ejemplo 11 dentro de la descripción de la función OnStart de su script y compile el archivo. Es probable que la recopilación tenga éxito.

A continuación, pruebe a descomentar la línea "miVariable.a = 5;" y vuelva a compilar el código. En este caso, obtendrá un error de compilación informando de un intento de acceso a miembros privados de la clase. Es esta característica del compilador la que eliminará algunos errores difíciles de ver que los programadores pueden cometer al trabajar con otros enfoques.

class PrivateAndPublic 
  {
private:
    int a;
public:
    int b;
  };

PrivateAndPublic myVariable;

// myVariable.a = 5; // Compiler error! 
myVariable.b = 10;   // Success

Ejemplo 11. Uso de las propiedades públicas y privadas de una clase.

Si tuviera que escribir todas las clases usted mismo, este enfoque no destacaría del resto, y no tendría mucho sentido.

Sin embargo, para suerte mía, hay muchas clases estándar que ya se encuentran en el directorio MQL5\Include. Además, en CodeBase se recogen bastantes bibliotecas muy útiles. En muchos casos, bastará con conectar el archivo correspondiente (como se describe a continuación) para aprovechar lo que han hecho otras personas inteligentes. Esto facilitará mucho el trabajo de los programadores.

Se han escrito libros enormes sobre la programación orientada a objetos, y sin duda esta merece un artículo aparte. Sin embargo, el propósito de este artículo es simplemente ofrecer al principiante una idea de cómo utilizar tipos de datos complejos en los programas que encontrará. Ahora ya sabe cómo describir una clase simple y cómo utilizar las clases de otras personas, así que pasaremos a la siguiente sección.


Tipo de datos funcional (operador typedef)

Advertencia. Esta sección puede resultar difícil para los principiantes, por lo que tal vez prefiera saltársela la primera vez que lea el artículo.

Comprender el material de esta sección no tendrá ningún impacto en el aprendizaje del resto del material, y posiblemente incluso en su camino restante en la programación. La mayoría de las tareas pueden tener múltiples soluciones, por lo que es bastante posible prescindir de los tipos funcionales.

Sin embargo, la posibilidad de asignar ciertas funciones a una variable (y, en consecuencia, utilizarlas como argumentos de otras funciones en algunos casos) sigue siendo cómoda, y creo que debería conocer esta característica, aunque solo sea para poder leer el código de otras personas.

A veces resulta útil crear variables de tipo "funcional", por ejemplo, para transmitirlas como argumento a otra función.

Por ejemplo, en una situación de negociación, las órdenes de compra y venta resultan muy similares y solo difieren en un parámetro. No obstante, el precio de compra será siempre Ask, y la venta será siempre al precio Bid.

Los programadores suelen escribir sus propias funciones para comprar y para vender (Buy y Sell), que tienen en cuenta todos los matices de una orden concreta. Y luego también escriben una función como Trade, que combina estas dos características y tiene el mismo aspecto tanto al negociar "al alza" como "a la baja". Esto resulta muy cómodo porque el propio Trade sustituirá las llamadas de las funciones escritas Buy o Sell dependiendo de la dirección calculada del movimiento del precio, y el programador podrá centrarse en otra cosa.

Seguro que nos vienen a la cabeza muchas ocasiones en las que queremos decir: "Autómata, ¡hazlo tú mismo!", y dejar que la función decida por sí misma cuál de las opciones "dicotómicas" debe llamarse en una situación dada. Al calcular un "take profit", ¿debo sumar o restar el número de pips al precio? ¿Y al calcular un stop loss? Al colocar una orden según el extremo, ¿debo buscar máximos o mínimos? Y así sucesivamente.

Estos son los casos en los que a veces se usa el enfoque que describiremos a continuación.

Como de costumbre, primero deberemos describir el tipo de la variable deseada. En este caso, este tipo se describirá con el siguiente patrón:

typedef function_result_type (*Function_type_name)(input_parameter1_type,input_parameter1_type ...); 

Ejemplo 12. Plantilla para describir un tipo funcional.

Aquí:

  • function_result_type — tipo del valor de retorno (cualquiera válido, como int, double o cualquier otro);
  • Function_type_name — nombre del tipo que utilizaremos al crear variables;
  • input_parameter1_type — tipo del primer parámetro. Resulta evidente que la lista de parámetros obedece a las reglas de las listas habituales para las funciones.

Observe el asterisco (*) delante del nombre del tipo. Es esencial, y nada funcionará sin él.

Significa que una variable de este tipo no contendrá un resultado o un número, sino la propia función, que posee un conjunto completo de capacidades, y, por lo tanto, esta variable combinará las capacidades inherentes tanto a otras variables como a las funciones.

Una construcción de este tipo, que al describir un tipo de datos usa el propio objeto (una función, un objeto de alguna clase, etc.) en lugar de una copia de los datos del objeto o el resultado de su operación, se denominará puntero.

Hablaremos de los punteros en futuros artículos. Veamos un ejemplo práctico del uso del operador typedef.

Vamos a suponer que tenemos las funciones Diff y Add, que queremos asignar a alguna variable. Ambas funciones retornan valores enteros y admiten dos parámetros enteros cada una. Su realización es elemental:

//---
int Add (int a,int b)
  {
    return (a+b);
  }

//---
int Diff (int a,int b)
  {
    return (a-b);
  }

Ejemplo 13. Funciones de suma y diferencia para la comprobación de tipos funcionales.

Ahora describiremos el tipo TFunc, para las variables que pueden almacenar cualquiera de estas funciones:
typedef int (*TFunc) (int,  int);

Ejemplo 14. Tipo de descripción para variables capaces de almacenar funciones Add y Diff.


Y ahora comprobaremos cómo funcionará esta descripción:

void OnStart()
  {
    TFunc operate;       //As usual, we declare a variable of the described type
 
    operate = Add;       // Write a value to a variable (in this case, assign a function)
    Print(operate(3, 5)); // Use the variable as a normal function
                         // Function output: 8

    operate=Diff;
    Print(operate(3, 5)); // Function output: -2
  }

Ejemplo 15. Uso de una variable de tipo funcional.

Por último, querría señalar que el operador typedef solo funciona con funciones escritas de forma independiente.

No podemos usar directamente funciones estándar como MathMin o similares, pero podemos crear una "envoltura" para ellas. Por ejemplo:

//---
double MyMin(double a, double b){
   return (MathMin(a,b));
}

//---
double MyMax(double a, double b){
   return (MathMax(a,b));
}

//---
typedef double (*TCompare) (double,  double);

//---
void OnStart()
  {
    TCompare extremumOfTwo;

    compare= MyMin;
    Print(extremumOfTwo(5, 7));// 5

    compare= MyMax;
    Print(extremumOfTwo(5, 7));// 7
  }

Ejemplo 16. Uso de "envoltorios" para trabajar con funciones estándar.


Inclusión de archivos externos (directiva #include del preprocesador)

Cualquier programa puede dividirse en ciertos módulos.

Si trabajamos con grandes proyectos, este desglose resulta imprescindible. La modularidad del programa resuelve varios problemas al mismo tiempo.

  • En primer lugar, lo necesitamos para facilitar la navegación por el código.
  • En segundo lugar, si trabajamos en equipo, cada módulo puede ser descrito por diferentes personas, lo que acelera mucho el desarrollo.
  • Bueno, y en tercer lugar, los módulos ya escritos pueden reutilizarse.

El "módulo" más obvio serían las funciones. Sin embargo, un módulo independiente también puede contener "todas las constantes", o la descripción de algún tipo de dato complejo, o combinar varias funciones relacionadas con un ámbito de actividad (por ejemplo, funciones para cambiar la apariencia de los objetos o funciones matemáticas)....

En proyectos de gran envergadura, resulta muy cómodo colocar estos bloques de código en archivos independientes e integrarlos en el programa actual.

La directiva #include del preprocesador se usará para incluir archivos de texto adicionales en un programa:

#include <SomeFile.mqh>     // Angle brackets specify search relative to MQL5\Include directory 
#include "AnyOtherPath.mqh" // Quotes specify search relative to current file

Ejemplo 17. Dos formas de la directiva #include.

Si el compilador encuentra una instrucción #include en cualquier parte de su código, intentará insertar el contenido del archivo especificado en lugar de esa instrucción, pero solo una vez por programa. Si el archivo ya se ha usado, no se conectará una segunda vez.

Podemos probar esta afirmación utilizando el script descrito en la siguiente sección.

En la mayoría de los casos, los archivos de inclusión tienen la extensión *.mqh por comodidad, pero en general la extensión puede ser cualquiera.


Script para comprobar el funcionamiento de la directiva #include

Con el fin de comprobar las acciones del compilador cuando se encuentra esta directiva de preprocesador, crearemos dos archivos.

En primer lugar, crearemos un archivo llamado "1.mqh" en el catálogo de scripts (MQL5\Scripts). El contenido de este archivo será muy sencillo:

Print("This is include with number "+i);

Ejemplo 18. El archivo de inclusión más sencillo solo puede contener un comando.

Creo que está claro lo que hace este código. Asumiendo que la variable i ha sido descrita en alguna parte, este código creará un mensaje para el usuario, añadirá el valor de la variable al mensaje, y luego enviará ese mensaje al log.

La variable i es un marcador que indicará en qué parte del script se ha activado (o no) la llamada a esta instrucción. Una vez más, no será necesario escribir nada más en este archivo. Ahora en el mismo directorio (el mismo lugar donde se encuentra el archivo "1.mqh"), crearemos un script que contendrá el siguiente código:

//+------------------------------------------------------------------+ 
//| Script program start function                                    | 
//+------------------------------------------------------------------+ 
void OnStart() 
  { 
    //---   
    int i=1; 
#include "1.mqh"   
    i=2; 
#include "1.mqh" 
  } 
//+------------------------------------------------------------------+

// Script output:
// 
//   This is include with number 1
//
// The second attempt to use the same file will be ignored

//+------------------------------------------------------------------+ 

Ejemplo 19. Estudio sobre la inclusión repetida de archivos.

En este código, intentaremos utilizar el archivo "1.mqh" dos veces para obtener dos mensajes de activación.

Cuando ejecutemos este script en el terminal, veremos que el primer mensaje funciona como se esperaba, mostrando el número 1 en el mensaje, pero el segundo mensaje no aparece.

¿Por qué es tan complicado esto? ¿Por qué no pegar el contenido cada vez?

Se trata de un principio importante porque los archivos de inclusión suelen contener descripciones de variables y funciones. Ya sabe que en un programa solo debe haber una variable con un nombre concreto a nivel global (fuera de todas las funciones).

Si, por ejemplo, describimos una variable int a; no podremos describir exactamente la misma variable una segunda vez en este nivel, solo podemos utilizar la que tenemos. Las funciones son un poco más complicadas, pero la cuestión es la misma: cada función deberá ser única dentro de nuestro programa. Imaginemos ahora que el programa usa dos módulos independientes, pero en cada uno de ellos se conecta la misma clase estándar ubicada en el archivo <Arrays\List.mqh> (figura 2).

Usamos una clase con dos módulos

Figura 2. Usamos una clase con dos módulos.

Si no existiera la "regla de una sola vez", el compilador generaría un mensaje de error porque está prohibido describir dos veces la misma clase. Pero en este caso la construcción resulta bastante viable, porque después de la descripción del campo FieldOf_Module1, la descripción CList ya está incluida en las listas del compilador, por eso solo utilizará esa descripción para el módulo 2.

Si comprendemos este principio, no tendremos miedo de crear incluso inserciones "multicapa", por ejemplo, cuando algunos elementos de las clases dependen unos de otros "cíclicamente", como en la figura 3.

Incluso podemos describir una variable de la misma clase dentro de una clase.

Todas estas son construcciones válidas, y podemos crearlas, precisamente porque #include se activará estrictamente una vez para un único archivo.

Dependencia cíclica: cada clase contiene elementos que dependen de otra clase

Figura 3. Dependencia cíclica: cada clase contiene elementos que dependen de otra.

Para concluir esta sección, querría recordarle una vez más que los archivos de la biblioteca estándar de MetaTrader que podemos conectar a nuestro código, se encuentran en el directorio MQL5/Include. Para abrir fácilmente este catálogo en el explorador, podemos seleccionar el menú "Archivo"->"Abrir catálogo de datos" en el terminal MetaTrader (figura 4).

Pasamos al catálogo de datos

Figura 4. Pasamos al catálogo de datos.

Si desea consultar los archivos de este catálogo en el MetaEditor, bastará con encontrar la carpeta Include en el panel del navegador.  Podemos crear nuestro propios archivos de inclusión en el mismo directorio (preferiblemente en carpetas separadas), o podemos utilizar un directorio de nuestro programa y sus subdirectorios (véanse los comentarios del ejemplo 17). Por regla general, las directivas #include se utilizan al principio del archivo, antes de cualquier otra acción. Sin embargo, esta "regla" no es estricta: todo dependerá de las tareas específicas que realicemos.


Conclusión

Una vez más, vamos a repasar brevemente los temas tratados en este artículo.

  1. Hemos descrito la directiva de preprocesador #include. Esta nos permite incluir archivos de texto adicionales en nuestro programa, normalmente algunas bibliotecas.
  2. Hemos considerado los tipos de datos complejos: estructuras, uniones y objetos (variables basadas en clases), así como tipos de datos funcionales.

Esperamos que los tipos de datos descritos en este artículo le resulten "complejos" solo en su estructura, no en su aplicación.

A diferencia de los tipos simples, que están incorporados en el lenguaje, los tipos "complejos" deberán describirse primero, y solo después deberemos crear las variables. Pero una vez descrito un tipo, trabajar con él no diferirá esencialmente del trabajo con los tipos "simples" y se reducirá a crear variables y llamar a componentes (miembros) de estas variables (si los hay) o utilizar el nombre de la variable como nombre de una función si creamos un tipo funcional.

La inicialización de variables creadas mediante estructuras puede realizarse utilizando paréntesis.

Y ahora, con suerte, comprenderá que la posibilidad de crear sus propios tipos complejos y dividir el programa en módulos almacenados en archivos externos hará que el desarrollo de programas resulte flexible y cómodo.


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

Visualización de transacciones en un gráfico (Parte 1): Seleccionar un periodo para el análisis Visualización de transacciones en un gráfico (Parte 1): Seleccionar un periodo para el análisis
Aquí vamos a desarrollar un script desde cero que simplifica la descarga de pantallas de impresión de transacciones para analizar entradas comerciales. Toda la información necesaria sobre una única operación se puede mostrar cómodamente en un gráfico con la posibilidad de dibujar diferentes marcos temporales.
Desarrollamos un asesor experto multidivisa (Parte 14): Cambio de volumen adaptable en el gestor de riesgos Desarrollamos un asesor experto multidivisa (Parte 14): Cambio de volumen adaptable en el gestor de riesgos
El gestor de riesgos que hemos desarrollado en los últimos artículos solo contiene funciones básicas. Hoy trataremos de analizar sus posibles formas de desarrollo, lo que nos permitirá aumentar los resultados comerciales sin interferir con la lógica de las estrategias de negociación.
Operar con noticias de manera sencilla (Parte 2): Gestión de riesgos Operar con noticias de manera sencilla (Parte 2): Gestión de riesgos
En este artículo, se introducirá la herencia en nuestro código anterior. Se implementará un nuevo diseño de base de datos para brindar eficiencia. Además, se creará una clase de gestión de riesgos para abordar los cálculos de volumen.
Estrategia de trading del SP500 en MQL5 para principiantes Estrategia de trading del SP500 en MQL5 para principiantes
Descubra cómo aprovechar MQL5 para pronosticar el S&P 500 con precisión, combinando análisis técnico clásico para lograr mayor estabilidad y algoritmos con principios probados en el tiempo para obtener información sólida del mercado.