Corrección y depuración de errores

El arte de programar se basa en la capacidad de dar instrucciones al programa sobre qué debe hacer y cómo debe hacerlo, y también de protegerlo frente a la posibilidad de que haga algo mal. Esta última labor es, por desgracia, mucho más difícil de ejecutar debido a múltiples factores no muy obvios que afectan al comportamiento del programa. Datos incorrectos, recursos insuficientes, errores de codificación propios y ajenos son sólo algunos de los problemas.

Nadie está a salvo de cometer errores al codificar programas. Los errores pueden producirse en distintas fases y se dividen acertadamente en:

  • Errores de compilación devueltos por el compilador al identificar un código fuente que no cumple la sintaxis requerida (ya hemos visto tales errores más arriba); es más fácil solucionarlos porque el compilador se encarga de buscarlos;
  • Errores de ejecución del programa devueltos por el terminal, si se da una condición incorrecta en el programa, como la división por cero, el cálculo de la raíz cuadrada de un negativo o el intento de hacer referencia a un elemento inexistente del array, como en nuestro caso; son más difíciles de detectar, ya que normalmente no se producen en cualquier valor de los parámetros de entrada, sino sólo en condiciones específicas;
  • Errores de diseño del programa que conducen a su total apagado sin ningún dato por parte del terminal, como atascarse en un bucle infinito; estos errores pueden convertirse en los más complejos en términos de localización y reproducción, cuando la capacidad de reproducir un problema en el programa es una condición necesaria para solucionarlo a posteriori;
  • Errores ocultos, en los que el programa parece funcionar sin problemas, pero el resultado proporcionado no es correcto; es fácil detectar si 2*2 no es 4, mientras que es mucho más difícil darse cuenta de las discrepancias.

Pero volvamos a la situación concreta con nuestro script. Según el mensaje de error que nos proporciona el entorno de ejecución del programa MQL, la siguiente sentencia es incorrecta:

return messages[hour / 8]

Al calcular el índice de un elemento del array, dependiendo del valor de la variable hour, se puede obtener un valor que supere el tamaño del array de tres.

El depurador incorporado en MetaEditor permite asegurarse de que esto realmente sucede así. Todos sus comandos están recogidos en el menú de depuración y ofrecen muchas funciones útiles. Aquí sólo nos vamos a fijar en dos: Depurar -> Empezar con datos reales (F5) y Depurar -> Empezar con datos históricos (Ctrl+F5). Puede obtener información sobre los otros en la Ayuda de MetaEditor.

Ambos comandos compilan el programa de una manera especial: con la información de depuración. Dicha versión del programa no está optimizada como en la compilación estándar (para obtener más detalles sobre la optimización, consulte Documentación), mientras que, al mismo tiempo, permite utilizar la información de depuración para «mirar dentro» del programa durante la ejecución: vea los estados de las variables y las pilas de llamadas a funciones.

La diferencia entre la depuración en datos reales y en datos históricos consiste en iniciar el programa en un gráfico en línea con la primera y en el gráfico de prueba en modo visual con la segunda. Para indicar al editor qué gráfico exactamente debe utilizar y con qué ajustes, es decir, símbolo, marco temporal, intervalo de fechas, etc., debe abrir para empezar el cuadro de diálogo Ajustes -> Depurar y rellenar en él los campos necesarios. La opción Usar los ajustes especificados debe estar activada. Si está desactivada, el primer símbolo de Market Watch y el marco temporal H1 se utilizarán en la depuración en línea, mientras que los ajustes del comprobador se utilizan al realizar la depuración en los datos históricos.

Tenga en cuenta que en el probador sólo se pueden depurar indicadores y Asesores Expertos. Sólo la depuración en línea está disponible para los scripts.

Vamos a ejecutar nuestro script utilizando F5 e introduciendo 100 en el parámetro GreetingHour para reproducir la situación problemática anterior. El script comenzará a ejecutarse y el terminal mostrará prácticamente de inmediato un mensaje de error, así como la solicitud para abrir el depurador.

Critical error while running script 'GoodTime1 (EURUSD,H1)'.
Array out of range.
Continue in debugger?

Una vez que respondemos afirmativamente, entramos en MetaEditor, donde aparece destacada la cadena actual del código fuente en la que se ha producido el error (fíjese en la flecha verde del campo de la izquierda).

MetaEditor en modo depuración en caso de error

MetaEditor en modo depuración en caso de error

La pila de llamadas actual se muestra en la parte inferior izquierda de la ventana: aquí se enumeran todas las funciones (en orden ascendente) que han sido invocadas antes de que la ejecución del código se detuviera en la cadena actual. En particular, en nuestro script, la función OnStart fue invocada (por el propio terminal), y la función Greeting fue invocada desde él (nosotros la invocamos desde nuestro código). En la parte inferior derecha de la ventana se muestra un panel de resumen. En él se pueden introducir los nombres de las variables, o las expresiones completas en la columna Expression, y ver sus valores en las columnas Values de la misma cadena.

Por ejemplo, podemos utilizar el comando Añadir del menú contextual o hacer doble clic con el ratón en la primera cadena libre para introducir la expresión «hora / 8» y asegurarnos de que es igual a 12.

Dado que la depuración se detuvo como consecuencia de un error, no tiene sentido continuar el programa; por tanto, podemos ejecutar el comando Depurar -> Parar (Mayús+F5).

En casos más complejos de una fuente de problemas no tan obvia, el depurador permite el seguimiento cadena a cadena de la secuencia de ejecución de las sentencias y del contenido de las variables.

Para resolver el problema es necesario asegurarse de que, en el código, el índice del elemento siempre cae dentro del rango de 0-2, es decir, se ajusta al tamaño del array. En sentido estricto, deberíamos haber escrito algunas sentencias adicionales comprobando que los datos introducidos eran correctos (en nuestro caso, GreetingHour sólo puede tomar un valor dentro del rango de 0-23), y luego mostrar un aviso o arreglarlo automáticamente en caso de violación de las condiciones.

En este proyecto introductorio no iremos más allá de una simple corrección: mejoraremos la expresión que calcula el índice del elemento para que su resultado siempre caiga dentro del rango requerido. Para ello, vamos a conocer un operador más: el operador módulo, que sólo funciona para números enteros. Para indicar esta operación se utiliza el símbolo «%». El resultado de la operación módulo es el resto de la división entera del dividendo por el divisor. Por ejemplo:

11 % 5 = 1

Aquí, con la división entera de 11 entre 5, obtendríamos 2, que se corresponde con el mayor factor de 5 dentro de 11, que es 10. El resto entre 11 y 10 es exactamente 1.

Para solucionar el error en la función Greeting basta con realizar previamente la división de módulo de hour entre 24, lo que garantizará que el número de la hora esté comprendido entre 0 y 23. La función Greeting tendrá el siguiente aspecto:

string Greeting(int hour)
{
  string messages[3] = {"Good morning""Good afternoon""Good evening"};
  return messages[hour % 24 / 8];
}

Aunque esta corrección seguramente funcionará bien (vamos a comprobarlo en un minuto), no afecta a otro problema que queda fuera de nuestro enfoque. La cuestión es que el parámetro GreetingHour es del tipo int, es decir, puede tomar valores tanto positivos como negativos. Si intentáramos introducir -8, por ejemplo, o un número «más negativo», obtendríamos el mismo error de ejecución, es decir, sobrepasar el array; sólo que, en este caso, el índice no sobrepasa el valor más alto (tamaño del array) sino que se hace más pequeño que el más bajo (en concreto, -8 lleva a referirse al elemento -1, curiosamente, los valores de -7 a -1 se muestran en el elemento 0 y no provocan ningún error).

Para solucionar este problema, sustituiremos el tipo del parámetro GreetingHour por el entero sin signo: utilizaremos uint en lugar de int (hablaremos de todos los tipos disponibles en la segunda parte, y aquí es uint lo que necesitamos). Guiado por el límite para la no negatividad de los valores, incorporado a nivel del compilador para uint, MQL5 garantizará de forma independiente que ni el usuario (en el cuadro de diálogo de propiedades) ni el programa (en su cálculo) «se vuelvan negativos».

Vamos a guardar la nueva versión del script como GoodTime2, compilarla y lanzarla. Introducimos el valor 100 para el parámetro GreetingHour y nos aseguramos de que, esta vez, el script se ejecute sin errores, mientras se imprime el saludo «Buenos días» en el registro del terminal. Este es el comportamiento esperado (correcto), ya que podemos utilizar una calculadora y comprobar que el resto de la división de módulo de 100 entre 24 da 4, mientras que la división entera de 4 entre 8 es 0, lo que significa por la mañana, en nuestro caso. Desde el punto de vista del usuario, por supuesto, este comportamiento puede considerarse inesperado. Sin embargo, introducir 100 como número de hora también fue una acción inesperada del usuario. El usuario probablemente pensó que nuestro programa fallaría. Pero esto no sucedió, y eso es positivo. Por supuesto, con programas reales, los valores introducidos deben ser validados y el usuario debe ser informado de los errores.

Como medida adicional para evitar introducir un número incorrecto, también utilizaremos una función especial de MQL5 para dar un nombre más detallado y fácil de usar al parámetro de entrada. Para ello, utilizaremos un comentario después de la descripción del parámetro de entrada en la misma cadena. Por ejemplo, como este:

input uint GreetingHour = 0// Greeting Hour (0-23)

Tenga en cuenta que hemos escrito las palabras del nombre de la variable por separado en el comentario (ya no es un identificador en el código, sino un consejo para el usuario). Además, hemos añadido entre paréntesis el intervalo de valores válidos. Al lanzar el script, el anterior GreetingHour aparecerá en el cuadro de diálogo para introducir los parámetros de la siguiente manera:

Greeting Hour (0-23)

Ahora podemos estar seguros de que, si se introduce 100 como hora, no es culpa nuestra.

Un lector atento puede preguntarse por qué hemos definido la función Greeting con el parámetro hour y le enviamos GreetingHour si podríamos utilizar en ella directamente el parámetro de entrada. La función, como fragmento lógico discreto de un código, sirve tanto para dividir el programa en partes visibles y fáciles de entender como para reutilizarlas posteriormente. Las funciones se suelen llamar desde varias partes del programa o forman parte de una biblioteca que está conectada a varios programas diferentes. Por tanto, una función escrita correctamente debe ser independiente del contexto externo y puede moverse de unos programas a otros.

Por ejemplo, si necesitamos transferir nuestra función Greeting a otro script, dejará de compilarse, ya que no contendrá el parámetro GreetingHour. No es del todo correcto exigir que se añada, porque el otro script puede calcular el tiempo de manera diferente. En otras palabras: al escribir una función, debemos hacer todo lo posible por evitar dependencias externas innecesarias. En lugar de ello, debemos declarar los parámetros de la función que se puedan rellenar con el código de llamada.