Português
preview
Del básico al intermedio: Clases (III)

Del básico al intermedio: Clases (III)

MetaTrader 5Ejemplos |
29 0
CODE X
CODE X

Introducción

En el artículo anterior, Del básico al intermedio: Clases (II), intenté explicar uno de los temas más confusos de la programación orientada a objetos: el concepto y el uso básico de un destructor. También es, casi con toda seguridad, uno de los temas más difíciles. Sin embargo, allí solo vimos la parte más sencilla y agradable de una cuestión mucho más compleja. Lo explicado allí solo se aplica a una pequeña parte de lo que realmente ocurre en la práctica.

La dificultad de la programación orientada a objetos no radica en la programación en sí, sino en la definición deficiente de algunos conceptos necesarios para aplicarla correctamente. Muchos académicos los explican de forma superficial, cuando no completamente inadecuada. Tú, mi querido lector, debes dominarlos para comprender y aprovechar todo lo que puede ofrecernos la programación orientada a objetos. Publico ahora este material introductorio, pues necesitaremos los conceptos expuestos para continuar con la implementación del mecanismo de árboles. Hicimos una breve pausa en esa implementación para exponer con claridad estos conceptos de programación orientada a objetos.

Es muy probable que este artículo cierre la primera etapa de las explicaciones necesarias. Así podremos retomar la implementación del mecanismo de árboles desde el punto en que nos detuvimos. Sin más preámbulos, pasemos al tema principal del presente artículo.


Clases (III)

En el artículo anterior implementamos dos variantes del código: un script y un indicador. Ambos utilizaban el mismo archivo de encabezado. En consecuencia, al ejecutar ambos códigos en MetaTrader 5, ambos podían tener un comportamiento muy similar al utilizar el código de la clase definida en el archivo de encabezado.

Retomemos el punto en que nos detuvimos en el artículo anterior para que lo que explicaré aquí tenga sentido. Hagamos lo siguiente: revisemos los códigos del artículo anterior. Puedes verlos íntegramente a continuación.

01. //+------------------------------------------------------------------+
02. class C_Regression
03. {
04.     private :
05. //+----------------+
06.     public  :
07. //+----------------+
08.         C_Regression()
09.         {
10.             datetime    dt0 = TimeCurrent(),
11.                         dt1[20];
12. 
13.             CopyTime(NULL, NULL, dt0, dt1.Size(), dt1);
14.             ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0);
15.             ChartRedraw();
16.         }
17. //+----------------+
18.         ~C_Regression()
19.         {
20.             ObjectDelete(0, def_NameChannel);
21.             ChartRedraw();
22.         }
23. //+----------------+
24. };
25. //+------------------------------------------------------------------+

Código 01

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 01.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression channel;
11.     
12.     Sleep(2000);
13. }
14. //+------------------------------------------------------------------+

Código 02

01. //+------------------------------------------------------------------+
01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. #property description "Demo"
04. //+------------------------------------------------------------------+
05. #define def_NameChannel    "Demo"
06. //+------------------------------------------------------------------+
07. #include <Tutorial\File 01.mqh>
08. //+------------------------------------------------------------------+
09. C_Regression gl_Channel;
10. //+------------------------------------------------------------------+
11. int OnInit()
12. {
13.     return INIT_SUCCEEDED;
14. };
15. //+------------------------------------------------------------------+
16. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
17. {
18.     return rates_total;
19. };
20. //+------------------------------------------------------------------+
21. void OnDeinit(const int reason)
22. {
23. };
24. //+------------------------------------------------------------------+

Código 03

Bien, el código 01 corresponde al archivo de encabezado, el código 02 al script y el código 03 al indicador. Tanto el script como el indicador se comportan de forma similar. Ambos añaden al gráfico el objeto definido en la línea 14 del código 01: una línea de regresión. Aun así, funcionan de manera ligeramente distinta. El script mantiene la línea de regresión en el gráfico durante el período indicado en la línea 12 del código 02. En cambio, el indicador la mantiene mientras permanezca en el gráfico. En el caso del indicador, tú decides cuándo se elimina la línea de regresión, ya que desaparece en cuanto retiras el indicador.

El comportamiento del script y del indicador es muy conveniente y parece perfecto. Sin embargo, a pesar de que todo parece funcionar perfectamente, existe un pequeño problema. Al utilizar la clase como se hace en los códigos 02 y 03, tú, como programador, no controlas adecuadamente su ciclo de vida.

No me malinterpretes, mi querido lector. Digo que no tienes el control porque no puedes indicar cuándo debe crearse el objeto ni cuándo debe destruirse. El compilador decide ambos momentos. Tú solo defines cómo se comportarán los objetos de esa clase, pero no cuándo se crearán ni durante cuánto tiempo existirán. Sé que la falta de control sobre el ciclo de vida de los objetos resulta bastante extraña y difícil de creer. Sientes que controlas lo que hace el código y cómo funciona. Sin embargo, si comprendiste correctamente lo explicado en los dos últimos artículos, sabes muy bien que el control del programador no es total.

Cuando declaras una variable de un tipo de clase, el compilador llama al constructor adecuado para inicializar el objeto asociado. Del mismo modo, cuando finaliza el ciclo de vida de la variable declarada, el compilador llama al destructor. Así, el destructor completa correctamente el ciclo de vida del objeto de la forma que tú, como programador, hayas previsto.

Sin embargo, todavía nos falta controlar el momento exacto en que se crea y se destruye el objeto. Si no controlamos esos momentos, todo empieza a acumularse con gran rapidez. Si no comprendiste el contenido de los artículos anteriores, te pido que te detengas y vuelvas a ellos hasta asimilarlo correctamente. La siguiente explicación puede confundirte por completo: ha llegado el momento de aprender a utilizar dos nuevos operadores. Ya los vimos anteriormente, pero aquí es donde la cuestión se complica de verdad. Me refiero a los operadores new y delete.

Bien, hasta donde sé, los operadores new y delete sirven para asignar memoria. ¿Correcto? Sí, mi querido lector. Sin embargo, la función de reserva de memoria se aplica principalmente a C++. En MQL5, los operadores new y delete permiten controlar el ciclo de vida de un objeto mediante una variable que lo referencia. Hasta donde he podido comprobar, no pueden utilizarse del mismo modo que en C++, lo cual, en cierto modo, es positivo. Esta limitación aparente respecto de C++ reduce ciertos problemas que otros programadores podrían tener al interpretar el código.

Volvamos al tema. Para facilitar la explicación y evitar complicaciones innecesarias, trabajaremos únicamente con el código del indicador. Por ahora, nos centraremos en el código 03.

Como expliqué en el artículo anterior, cuando se ejecuta la línea nueve del código 03, el compilador llama al constructor de la clase, definido en la línea ocho del código 01. Más adelante, cuando la variable de la línea nueve del código 03 deje de utilizarse, el compilador llama al destructor de la clase, definido en la línea 18 del código 01. Así de sencillo. Sin embargo, podemos cambiar la forma en que se realizan estas llamadas. Basta con sustituir el código 03 por el que se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. #property description "Demo"
04. //+------------------------------------------------------------------+
05. #define def_NameChannel    "Demo"
06. //+------------------------------------------------------------------+
07. #include <Tutorial\File 01.mqh>
08. //+------------------------------------------------------------------+
09. C_Regression *gl_Channel;
10. //+------------------------------------------------------------------+
11. int OnInit()
12. {
13.     return INIT_SUCCEEDED;
14. };
15. //+------------------------------------------------------------------+
16. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
17. {
18.     return rates_total;
19. };
20. //+------------------------------------------------------------------+
21. void OnDeinit(const int reason)
22. {
23. };
24. //+------------------------------------------------------------------+

Código 04

Ahora, el código 04 se comportará de manera distinta del código 03. No veo ningún cambio. ¿Estás seguro de que el código 04 funciona de manera diferente? Para mí, los códigos 03 y 04 son iguales. No hay ninguna diferencia. Presta atención, mi querido lector. Sí existe una diferencia entre ambos códigos. Lee el código 04 con un poco más de atención.

¿Ya identificaste la diferencia? Sigo sin ver ninguna, salvo el asterisco que añadiste en el código 04. Aparte de ese asterisco, todo lo demás es igual. En efecto, ambos códigos parecen iguales, pero su comportamiento es distinto. El asterisco añadido cambió por completo el comportamiento del código 04. Ahora el compilador ya no llamará implícitamente al constructor. Es decir, declarar la variable de la línea nueve ya no basta para construir el objeto. Ahora debemos indicarle explícitamente al compilador:

Quiero que llames al constructor de la clase en este preciso momento.

Sin embargo, al llamar explícitamente al constructor, también tendremos que indicarle al compilador cuándo debe llamar al destructor. Esto se debe a que el compilador ya no sabe cuándo debe llamar al destructor. Vaya, qué complicado. Cuanto más leo tus artículos, más complicado se vuelve el tema. Creo que programar no es para mí. Me rindo. Tranquilo, mi querido lector, todavía ni siquiera hemos empezado a jugar de verdad. Vuelve, porque cada día el tema se pone aún más divertido. Lo que estamos viendo ahora es un juego de niños comparado con lo que realmente podemos hacer. ¿Y ya estás pensando en rendirte? (RISAS).

Bien, continuemos quienes no se rindieron. ¿Decidiste volver? Qué bien, me alegra ver esa determinación. Para demostrar que realmente controlaremos cuándo y en qué punto del código se invocan el constructor y el destructor, haremos una pequeña modificación en el archivo de encabezado. Los cambios se muestran a continuación.

01. //+------------------------------------------------------------------+
02. class C_Regression
03. {
04.     private :
05. //+----------------+
06.     public  :
07. //+----------------+
08.         C_Regression()
09.         {
10.             datetime    dt0 = TimeCurrent(),
11.                         dt1[20];
12. 
13.             CopyTime(NULL, NULL, dt0, dt1.Size(), dt1);
14.             ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0);
15.             ChartRedraw();
16.             Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__);
17.         }
18. //+----------------+
19.         ~C_Regression()
20.         {
21.             ObjectDelete(0, def_NameChannel);
22.             ChartRedraw();
23.             Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__);
24.         }
25. //+----------------+
26. };
27. //+------------------------------------------------------------------+

Código 05

Bien, ahora podemos ver directamente en el terminal cuándo se invocan el constructor y el destructor. Los mensajes generados por las líneas 16 y 23 del código 05 registran, respectivamente, la invocación del constructor y la del destructor. Para hacerlo aún más interesante, también modificaremos el código 04, que corresponde al indicador. El cambio se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. #property description "Demo"
04. //+------------------------------------------------------------------+
05. #define def_NameChannel    "Demo"
06. //+------------------------------------------------------------------+
07. #include <Tutorial\File 01.mqh>
08. //+------------------------------------------------------------------+
09. C_Regression *gl_Channel;
10. //+------------------------------------------------------------------+
11. int OnInit()
12. {
13.     Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__);
14.     gl_Channel = new C_Regression;
15. 
16.     return INIT_SUCCEEDED;
17. };
18. //+------------------------------------------------------------------+
19. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
20. {
21.     return rates_total;
22. };
23. //+------------------------------------------------------------------+
24. void OnDeinit(const int reason)
25. {
26.     delete gl_Channel;
27. 
28.     Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__);
29. };
30. //+------------------------------------------------------------------+

Código 06

Ahora presta muchísima atención, mi querido lector. Deja todo lo que estés haciendo y concéntrate únicamente en la explicación del artículo. La siguiente explicación marcará una gran diferencia a partir de ahora.

Observa las líneas 14 y 26 del código 06. Estamos haciendo algo que para la mayoría no tiene ningún sentido que para la mayoría no tiene ningún sentido, pero que para ti empezará a tenerlo. Hace poco afirmé que podemos controlar cuándo y dónde se construye un objeto de la clase, así como cuándo y dónde se destruye. Si la afirmación sobre el control del ciclo de vida es correcta, el mensaje generado en la línea 13 del código 06 aparecerá antes que el mensaje del constructor, generado en la línea 16 del código 05. Asimismo, el mensaje del destructor, generado en la línea 23 del código 05, aparecerá antes que el mensaje de la línea 28 del código 06. Durante el intervalo comprendido entre la llamada al constructor y la llamada al destructor, el objeto estará disponible para utilizarlo. Si estoy equivocado, los mensajes de las líneas 13 y 28 del código 06 aparecerán después de los mensajes del código 05. Para comprobar el orden de las llamadas, debemos ejecutar este indicador en un gráfico. La animación siguiente muestra el resultado.

Animação 01

Como la animación pasó demasiado rápido, capturaremos un instante concreto. La captura se muestra a continuación.

Imagem 01

Ahora observa atentamente los mensajes de la imagen 01. El primero indica que se está ejecutando OnInit en el archivo Code 01.mq5, en la línea 13. El primer mensaje coincide perfectamente con lo esperado, ya que el código 06 corresponde al archivo Code 01.mq5. El segundo mensaje indica que se está ejecutando el constructor en el archivo File 01.mqh, en la línea 16. El segundo mensaje también coincide con lo esperado, ya que el código 05 corresponde al archivo File 01.mqh. Por tanto, el objeto se inicializa en el momento y el lugar previstos.

Analicemos ahora la destrucción del objeto. El siguiente mensaje indica que se está ejecutando el destructor en el archivo File 01.mqh, en la línea 23. Perfecto. El último mensaje indica que se está ejecutando OnDeinit en el archivo Code 01.mq5, en la línea 28. Sencillamente fantástico. El código funcionó perfectamente y nos permitió decidir dónde y cuándo se crea y se destruye el objeto. Antes no teníamos control sobre el momento de creación y destrucción del objeto. Si no lo crees, hagamos lo siguiente: utilizaremos el código 03, que corresponde a la versión original del indicador, y volveremos a compilarlo con un pequeño cambio. Añadiremos los mismos mensajes del código 06. El código 03 quedará como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. #property description "Demo"
04. //+------------------------------------------------------------------+
05. #define def_NameChannel    "Demo"
06. //+------------------------------------------------------------------+
07. #include <Tutorial\File 01.mqh>
08. //+------------------------------------------------------------------+
09. C_Regression gl_Channel;
10. //+------------------------------------------------------------------+
11. int OnInit()
12. {
13.     Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__);
14. 
15.     return INIT_SUCCEEDED;
16. };
17. //+------------------------------------------------------------------+
18. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
19. {
20.     return rates_total;
21. };
22. //+------------------------------------------------------------------+
23. void OnDeinit(const int reason)
24. {
25.     Print("Running ", __FUNCTION__, " in ", __FILE__, " in line ", __LINE__);
26. };
27. //+------------------------------------------------------------------+
28. 

Código 07

Observa que el código 07 ya no utiliza los operadores new y delete, a diferencia del código 06. Sin embargo, el código 07 reproduce el comportamiento del código 03. Así podremos ver cuándo y dónde se crea y se destruye el objeto. La siguiente animación muestra la ejecución del código 07.

Animação 02

Como probablemente esta animación también haya pasado demasiado rápido, veamos su momento final. Puedes verlo con calma en la imagen siguiente.

Imagem 02

Ahora observa la imagen 02. El comportamiento registrado en la imagen 02 es claramente distinto del registrado en la imagen 01. En la imagen 02, el constructor se llama ANTES de que MetaTrader 5 invoque el manejador de eventos OnInit, y el destructor se llama DESPUÉS de que MetaTrader 5 invoque el manejador de eventos OnDeinit. La diferencia en el orden de las llamadas demuestra que un simple detalle en la implementación del código puede cambiar por completo el resultado final.

Bien, creo que estoy empezando a comprender cómo funciona todo esto y cómo debemos implementarlo. Pero ¿qué ocurre en el script? Lo pregunto porque la variable que referencia el objeto es global en el código del indicador y puede utilizarse desde cualquier punto del código del indicador. En cambio, la variable declarada en el código 02 del script es local. ¿El hecho de que la variable sea local produce alguna diferencia o, al final, todo funciona de la misma manera? Es una duda muy pertinente, mi querido lector. También es complicada de responder. Hasta ahora no habíamos tenido la oportunidad de explicarla correctamente, porque la respuesta depende de cómo pretendas utilizar el código de tu clase.

Intentaré explicar esta cuestión de una forma más fácil de asimilar. Afirmar simplemente que el script se comporta igual o de forma distinta que el indicador no ayudaría en absoluto; al contrario, generaría aún más dudas. Para analizar el comportamiento de ambos programas, tendremos que añadir algunos elementos al código. Pondré a tu disposición todos los archivos utilizados en el artículo para que puedas estudiarlos con calma. También crearemos otro archivo de encabezado. En principio, será una copia casi exacta del archivo de encabezado presentado como código 01. La creación del nuevo archivo de encabezado evitará dudas sobre por qué el código se implementa de una forma u otra y facilitará la comparación entre ambos archivos. El segundo archivo se muestra a continuación.

01. //+------------------------------------------------------------------+
02. class C_Regression
03. {
04.     private :
05. //+----------------+
06.     public  :
07. //+----------------+
08.         C_Regression()
09.         {
10.             datetime    dt0 = TimeCurrent(),
11.                         dt1[20];
12. 
13.             CopyTime(NULL, NULL, dt0, dt1.Size(), dt1);
14.             ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0);
15.             ChartRedraw();
16.             PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
17.         }
18. //+----------------+
19.         ~C_Regression()
20.         {
21.             ObjectDelete(0, def_NameChannel);
22.             ChartRedraw();
23.             PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
24.         }
25. //+----------------+
26.         void PrintMsg(const string msg)
27.         {
28.             Print(msg);
29.         }
30. //+----------------+
31. };
32. //+------------------------------------------------------------------+

Código 08

Observa que las líneas 16 y 23 del código 08 difieren ligeramente de las líneas 16 y 23 del código 05. Debido a las diferencias entre las líneas 16 y 23 de ambos códigos, utilizaremos el procedimiento de la línea 26 del código 08. Presta atención: el archivo 08 incluido en los anexos será distinto porque la modificaremos más adelante.

Bien, pasemos ahora al archivo que contiene el script. Introduciremos un pequeño cambio en su código, como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 02.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression channel;
11.     
12.     channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Sleep(2000);
15. }
16. //+------------------------------------------------------------------+

Código 09

Observa que, en la línea 12 del código 09, podemos enviar un mensaje a la clase mediante el método que acabamos de implementar. Hasta aquí, creo que ninguno de ustedes tendrá dudas sobre el mensaje que se imprimirá en el terminal. De todos modos, puedes verlo en la imagen siguiente.

Imagen 03

Bien, pasemos al primero de los problemas y veamos cómo resolverlo. En el artículo Del básico al intermedio: Sobrecarga, mencioné algunas formas de utilizar un mismo procedimiento o función, conservando su nombre, pero con objetivos diferentes. Aunque aquel artículo recibió poca atención, es importante que revises lo explicado allí. Ahora aplicaremos un principio muy parecido. Incluso creía haber hablado en otro artículo sobre lo que vamos a hacer. Si lo hice, no lo recuerdo, ya que parte del contenido se publicó en mi otro perfil de artículos. En cualquier caso, veamos cómo aplicar ese principio.

En el artículo Del básico al intermedio: Paso por valor o por referencia, expliqué algunos conceptos que ampliaremos aquí, ya que plantean una cuestión muy interesante que conviene examinar con detalle.

Ahora piensa en la siguiente situación, mi querido lector. Como ya debes saber, las variables de ámbito global nunca deberían ser tu primera opción. Lo ideal es comenzar siempre con variables de ámbito local. Expliqué brevemente el uso de variables globales y locales en los artículos sobre variables. El artículo Del básico al intermedio: Variables (II) te ayudará a comprender mejor la elección entre variables globales y locales. En el código 09 declaramos una variable local que nos permite referenciar la clase. Sin embargo, supongamos que necesitas acceder al objeto asociado con la variable local desde otro punto del código, por ejemplo, desde otro procedimiento.

Ya tenemos una variable local declarada en la línea diez del código 09; la variable declarada en la línea diez referencia un objeto. Crear otra instancia no garantiza que reproduzca el estado del objeto original. La nueva instancia solo coincidiría con la anterior mientras todavía no se hubiera modificado ningún valor, algo que normalmente no ocurre en la práctica. Por tanto, aparece el primer problema: debemos pasar al procedimiento la referencia declarada en la línea diez. Así, la función podrá acceder al objeto original. Podemos hacerlo como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 02.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression channel;
11.     
12.     channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Checking(channel);
15. 
16.     Sleep(2000);
17. }
18. //+------------------------------------------------------------------+
19. void Checking(C_Regression channel)
20. {
21.     channel.PrintMsg("Demonstrating the passage from a class to a procedure.");
22. }
23. //+------------------------------------------------------------------+

Código 10

Ahora presta atención a los detalles. En primer lugar, si intentas compilar el código 10, verás que el compilador muestra el siguiente mensaje:

Imagen 04

El mensaje del compilador indica que el parámetro de la función debe pasarse por referencia. Para resolver esta cuestión, modificamos el código como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 02.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression channel;
11.     
12.     channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Checking(channel);
15. 
16.     Sleep(2000);
17. }
18. //+------------------------------------------------------------------+
19. void Checking(C_Regression &channel)
20. {
21.     channel.PrintMsg("Demonstrating the passage from a class to a procedure.");
22. }
23. //+------------------------------------------------------------------+

Código 11

Ahora el código se compilará sin problemas. Sin embargo, existe un posible problema. Al pasar a un procedimiento o función la referencia a la clase, corremos el riesgo de que algo se modifique sin que lo advirtamos. No queremos que el procedimiento o la función realicen accidentalmente este tipo de modificación, ya sea a causa de una distracción o de cualquier otro motivo. En consecuencia, debemos restringir las operaciones permitidas sobre la instancia referenciada. Esto ya se explicó en los artículos anteriores, pero no abordaron un segundo paso que también debemos dar. Primero volveremos a modificar el script para que quede como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 02.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression channel;
11.     
12.     channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Checking(channel);
15. 
16.     Sleep(2000);
17. }
18. //+------------------------------------------------------------------+
19. void Checking(const C_Regression &channel)
20. {
21.     channel.PrintMsg("Demonstrating the passage from a class to a procedure.");
22. }
23. //+------------------------------------------------------------------+

Código 12

Bien, con la modificación del código 12 impedimos que el procedimiento o la función modifiquen accidentalmente algún dato de la clase. Sin embargo, el compilador muestra ahora otro error, que aparece a continuación.

Imagen 05

El error del compilador es realmente curioso y hasta gracioso en MQL5. En C++, en cambio, la situación puede volverse realmente peligrosa si el programador utiliza determinados recursos para eludir este error. Pero esos mecanismos de C++ no vienen al caso. Lo importante es que el compilador indica que intentamos invocar un método no constante mediante una referencia constante. En este contexto, el calificador const restringe las operaciones permitidas sobre la instancia referenciada: si la referencia no puede modificarse, tampoco podrán modificarse los valores accesibles mediante ella.

Debemos corregir la declaración del método para que el código siga funcionando correctamente. Aquí es donde entraría la sobrecarga. En este caso concreto no será necesario realizar la sobrecarga, porque no aportaría diferencias significativas al resultado final. Así pues, volvamos al archivo de encabezado y modifiquémoslo como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. class C_Regression
03. {
04.     private :
05. //+----------------+
06.     public  :
07. //+----------------+
08.         C_Regression()
09.         {
10.             datetime    dt0 = TimeCurrent(),
11.                         dt1[20];
12. 
13.             CopyTime(NULL, NULL, dt0, dt1.Size(), dt1);
14.             ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0);
15.             ChartRedraw();
16.             PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
17.         }
18. //+----------------+
19.         ~C_Regression()
20.         {
21.             ObjectDelete(0, def_NameChannel);
22.             ChartRedraw();
23.             PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
24.         }
25. //+----------------+
26.         void PrintMsg(const string msg) const
27.         {
28.             Print(msg);
29.         }
30. //+----------------+
31. };
32. //+------------------------------------------------------------------+

Código 13

Observa que solo necesitamos convertir en constante el método declarado en la línea 26. Al declararlo como método constante, el compilador permite invocarlo mediante la referencia constante para que el compilador pueda interpretar correctamente el código que queremos construir. Una vez compilado, verás el resultado en la animación siguiente.

Animación 03

Muy bien, ahora aplicaremos un último ajuste que se integra perfectamente en el contenido del presente artículo. También nos ayudará a entender cómo debemos actuar en determinadas situaciones. En los artículos anteriores vimos que podemos pasar argumentos al constructor para controlar la inicialización del sistema. Con lo explicado hasta ahora, seguramente ya sabes cómo actuar en la mayoría de las situaciones. Sin embargo, ¿qué debes hacer si necesitas utilizar los operadores new y delete para decidir cuándo y dónde se crea y se destruye el objeto? Pocos se plantean esta cuestión antes de enfrentarse directamente al problema antes de encontrarse con el problema, pero aquí veremos cómo resolverlo antes de que aparezca.

Para empezar, observa que en los códigos 12 y 13 no es necesario pasar ningún argumento al constructor. Ahora modificaremos el ejemplo para que sea necesario pasar un valor al constructor. Así sabremos cómo actuar si en el futuro necesitamos utilizar los operadores new y delete.

01. //+------------------------------------------------------------------+
02. class C_Regression
03. {
04.     private :
05. //+----------------+
06.     public  :
07. //+----------------+
08.         C_Regression(const string msg)
09.         {
10.             datetime    dt0 = TimeCurrent(),
11.                         dt1[20];
12. 
13.             CopyTime(NULL, NULL, dt0, dt1.Size(), dt1);
14.             ObjectCreate(0, def_NameChannel, OBJ_REGRESSION, 0, dt1[0], 0, dt0, 0);
15.             ChartRedraw();
16.             PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
17.             PrintMsg("Message received: " + msg);
18.         }
19. //+----------------+
20.         ~C_Regression()
21.         {
22.             ObjectDelete(0, def_NameChannel);
23.             ChartRedraw();
24.             PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
25.         }
26. //+----------------+
27.         void PrintMsg(const string msg) const
28.         {
29.             Print(msg);
30.         }
31. //+----------------+
32. };
33. //+------------------------------------------------------------------+

Código 14

En el código 14 ahora debemos pasar un argumento al constructor. Esa misma información se mostrará en el terminal mediante la línea 17. Veamos cómo queda el código del script.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 03.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression channel(StringFormat("Init in line %d", __LINE__));
11.     
12.     channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Checking(channel);
15. 
16.     Sleep(2000);
17. }
18. //+------------------------------------------------------------------+
19. void Checking(const C_Regression &channel)
20. {
21.     channel.PrintMsg("Demonstrating the passage from a class to a procedure.");
22. }
23. //+------------------------------------------------------------------+

Código 15

Observa atentamente la línea diez del código 15. Cuando ejecutemos este código, obtendremos el resultado que se muestra a continuación.

Imagen 06

Sencillamente perfecto. El resultado demuestra que todo funciona como habíamos previsto. Ahora incorporaremos los operadores new y delete al script. El cambio se muestra en el código siguiente.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 03.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression *channel(StringFormat("Init in line %d", __LINE__));
11.     
12.     channel.PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Checking(channel);
15. 
16.     Sleep(2000);
17. }
18. //+------------------------------------------------------------------+
19. void Checking(const C_Regression &channel)
20. {
21.     channel.PrintMsg("Demonstrating the passage from a class to a procedure.");
22. }
23. //+------------------------------------------------------------------+

Código 16

Al intentar compilar el código 16, el compilador mostrará el siguiente mensaje.

Imagen 07

Es decir, la declaración de la línea diez del código 16 es incorrecta. El problema no se debe a que estemos haciendo algo parecido a lo realizado en el código 04. El problema es que NO ES ASÍ como debemos inicializar el objeto. La diferencia es la siguiente: el código 04 se compilará, aunque no mostrará ningún resultado en el gráfico. En cambio, el código 16 NO SE COMPILARÁ porque la declaración de su línea diez intenta inicializar el objeto de forma incorrecta.

Para que el constructor reciba un argumento mediante una declaración similar a la utilizada en el código 04, debemos utilizar un procedimiento ligeramente distinto. Para que el código 16 pueda compilarse, debemos estructurar la declaración como se muestra a continuación.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define def_NameChannel    "Demo"
05. //+------------------------------------------------------------------+
06. #include <Tutorial\File 03.mqh>
07. //+------------------------------------------------------------------+
08. void OnStart(void)
09. {
10.     C_Regression *channel = new C_Regression(StringFormat("Init in line %d", __LINE__));
11.     
12.     (*channel).PrintMsg(StringFormat("Running %s in %s in line %d", __FUNCTION__, __FILE__, __LINE__));
13. 
14.     Checking(channel);
15. 
16.     Sleep(2000);
17. 
18.     delete channel;
19. }
20. //+------------------------------------------------------------------+
21. void Checking(const C_Regression &channel)
22. {
23.     channel.PrintMsg("Demonstrating the passage from a class to a procedure.");
24. }
25. //+------------------------------------------------------------------+

Código 17

Normalmente, puedes dividir la declaración de la línea diez del código 17 en dos etapas. Sin embargo, como quiero inicializar el objeto al declarar la variable, resulta más sencillo escribir la declaración como se muestra anteriormente. Aun así, estamos haciendo algo muy parecido a lo realizado en el código 06. Mediante los operadores new y delete, controlamos dónde y cómo se inicializa y se destruye la clase.

La forma de pasar argumentos al constructor no cambia. Solo debemos realizar algunos ajustes adicionales. Tras aplicarlos, obtenemos el resultado que se muestra a continuación.

Imagen 08


Consideraciones finales

En el presente artículo hemos visto cómo controlar mejor el código cuando utilizamos programación orientada a objetos. Todavía estamos apenas al comienzo de todo lo que puede abordarse sobre programación orientada a objetos. Sin embargo, lo explicado hasta ahora ya nos permite retomar la implementación de los árboles. Así que, mi querido lector, practica y estudia con mucha calma y dedicación los conceptos presentados en los tres últimos artículos de la serie. Estos conceptos te acompañarán durante el resto de tu vida en MQL5. También te facilitarán el camino si más adelante deseas aprender a programar en C++.

Por lo demás, me despido por ahora. Nos veremos en el próximo artículo, donde retomaremos el tema de las colas, listas y árboles.

Archivo MQ5Descripción
Indicador\Code 01 Archivo de demostración
Indicador\Code 02  Archivo de demostración
Script\Code 01  Archivo de demostración
Script\Code 02 Archivo de demostración
Script\Code 03  Archivo de demostración
Script\Code 04  Archivo de demostración

Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/16765

Archivos adjuntos |
Anexo.zip (4.58 KB)
Introducción a MQL5 (Parte 26): Creación de un EA basado en zonas de soporte y resistencia Introducción a MQL5 (Parte 26): Creación de un EA basado en zonas de soporte y resistencia
Este artículo te enseña cómo crear un Asesor Experto MQL5 que detecta automáticamente las zonas de soporte y resistencia y ejecuta operaciones basándose en ellas. Aprenderás a programar tu EA para identificar estos niveles clave del mercado, monitorear las reacciones de los precios y tomar decisiones de trading sin intervención manual.
Simulación de mercado: Position View (XVI) Simulación de mercado: Position View (XVI)
En este artículo realizaremos las modificaciones necesarias para que el indicador de posición muestre un resultado financiero. De este modo, el operador podrá hacerse una idea del resultado financiero que se obtiene con una posición abierta. Además, compartiré contigo algo que muchos desconocen, incluso quienes llevan mucho tiempo utilizando MQL5: cómo utilizar variables estáticas para compartir memoria y evitar declarar una variable global en el código principal.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Del básico al intermedio: Clases (II) Del básico al intermedio: Clases (II)
Este artículo busca ser lo más didáctico posible, porque el tema que abordaremos ya suele generar bastante confusión. Por eso, mi querido lector, intenta poner en práctica lo que se explica aquí. Ante cualquier duda, no dejes de comentar, pues comprender los destructores no es precisamente una de las tareas más sencillas.