Contexto, ámbito y vida útil de las variables

MQL5 está entre los lenguajes de programación que utilizan llaves para agrupar las sentencias en bloques de código.

Recordemos que un programa se compone de bloques con sentencias, y un bloque debe existir de forma inequívoca. En los ejemplos de script de la Parte 1 vimos la función OnStart. El cuerpo de esta función (el texto entre llaves que sigue al nombre de la función) es exactamente ese bloque de código necesario.

Dentro de cada bloque se forma el contexto local, es decir, una región que limita la visibilidad y la vida útil de las variables descritas en su interior. Hasta ahora sólo hemos encontrado ejemplos en los que las llaves definen el cuerpo de las funciones; sin embargo, también pueden utilizarse para formar operadores compuestos, en la sintaxis de la descripción de clases y espacios de nombres. Todos estos métodos también definen regiones de visibilidad y se estudiarán en las secciones correspondientes. En esta fase sólo tenemos en cuenta un tipo de bloques locales, los que están dentro de funciones.

Además de regiones locales, cada programa tiene también un contexto global, es decir, una región con las definiciones de variables, funciones y otras entidades hechas más allá de otros bloques.

Por el lado del script sencillo, en el que el Asistente MQL ha creado la única función OnStart vacía, habrá entonces sólo 2 regiones: una global y otra local (dentro del cuerpo de la función OnStart , aunque esté vacía). El siguiente script lo ilustra con comentarios.

// GLOBAL SCOPE
void OnStart()
{
  // LOCAL SCOPE "OnStart"
}
// GLOBAL SCOPE

Tenga en cuenta que la región global se extiende por todas partes aparte de la función OnStart (tanto antes como después de ella). Básicamente, incluye todo más allá de cualquier función (si hubiera muchas), pero no hay nada en este script, aparte de OnStart.

Podemos describir variables, como i, j, k, en la parte superior del archivo, y se convertirán en globales.

// GLOBAL SCOPE
int ijk;
void OnStart()
{
  // LOCAL SCOPE "OnStart"
}
// GLOBAL SCOPE

Las variables globales se crean inmediatamente al iniciar un programa MQL en el terminal y existen durante todo el periodo de ejecución del programa.

El programador puede registrar y leer el contenido de las variables globales desde cualquier lugar del programa.

Básicamente se recomienda describir las variables globales sólo al principio, pero es necesario. Si movemos la declaración debajo de toda la función OnStart, no cambiará nada básicamente; sólo será difícil para otros programadores encontrarle el sentido al instante al código con variables, a cuyas definiciones primero hay que llegar.

Curiosamente, la propia función OnStart también se declara en el contexto global. Si añadimos otra función, ésta también se declarará en el contexto global. Recuerde cómo creamos la función Greeting en la Parte 1 y cómo la invocamos desde la función OnStart. Este es el efecto de que el nombre de la función y el método de hacer referencia a ella (cómo ejecutarla) se conozcan en todo el código fuente. Los espacios de nombres añaden algunas sutilezas, pero las veremos más adelante.

Una región local dentro de cada función pertenece sólo a dicha función: una región local está dentro de OnStart, y otra está dentro de Greeting, que es propia y difiere tanto de la región local de OnStart como de la global.

Las variables descritas en el cuerpo de la función se denominan locales y se crean según sus descripciones a partir de la llamada a la función correspondiente durante la ejecución del programa. Las variables locales sólo pueden utilizarse dentro del bloque que las contiene. No son visibles ni accesibles desde el exterior. Al salir de la función, las variables locales se destruyen.

Ejemplo de descripción de las variables x, y, z locales dentro de la función OnStart:

// GLOBAL SCOPE
int ijk;
void OnStart()
{
  // LOCAL SCOPE "OnStart"
  int xyz;
}
// GLOBAL SCOPE

Debe tenerse en cuenta que los pares de llaves pueden utilizarse tanto para describir la función y otras sentencias como para formar por sí mismas el bloque de código interno. El anidamiento de unidades es ilimitado.

Los bloques anidados suelen añadirse para minimizar el alcance de las variables utilizadas en una pequeña ubicación de código lógicamente aislada (si no es establecida por una función por un motivo u otro). Esto permite reducir la probabilidad de una falsa modificación de la variable donde no estaba prevista o algunos efectos secundarios no deseados debidos al intento de reutilizar la misma variable para diversas necesidades (lo que no es una buena práctica).

A continuación se muestra una función de ejemplo en la que el nivel de anidamiento de unidades es 2 (si consideramos que el bloque con el cuerpo de la función es el primer nivel) y se crean 2 bloques de este tipo que se ejecutarán consecutivamente.

void OnStart()
{
  // LOCAL SCOPE "OnStart"
  int xyz;
  
  { 
    // LOCAL SUBSCOPE 1
    int p;
    // ... use p for task 1
  }
  
  { 
    // LOCAL SUBSCOPE 2
    // y = p; // error: 'p' - undeclared identifier
    int p;    // from now 'p' is declared
    // ... use p for task 2
  }
  
  // p = x; // error: 'p' - undeclared identifier
}

Dentro de ambos bloques se describe la variable p, que en ellos se utiliza para diversos fines. De hecho, se trata de dos variables diferentes, aunque tengan el mismo nombre visible dentro de cada bloque.

Si la variable se sacara a la lista inicial de las variables locales de la función, podría contener algún valor restante al salir del primer bloque, rompiendo así el funcionamiento del segundo bloque. Además, el programador podría implicar ocasionalmente a p en algo más al principio de la función, y entonces los efectos secundarios podrían tener lugar en el primer bloque.

Más allá de cualquiera de los dos bloques anidados, la variable p es desconocida y, por tanto, cualquier intento de referirse a ella desde el bloque común de la función conduce a un error de compilación («identificador no declarado»).

También hay que tener en cuenta que una variable puede describirse no al principio del bloque, sino en su mitad o incluso más cerca del final. Entonces no se define en todo el bloque, sino sólo por debajo de su definición. Por lo tanto, se producirá el mismo error cuando se haga referencia a la variable por encima de su descripción.

Así, la región de ámbito de la variable puede diferir del contexto (todo el bloque).

Ambas versiones del problema se ilustran en un ejemplo: intente incluir cualquiera de las cadenas con las sentencias p = x y y = p y compile el código fuente.

Se asigna memoria a todas las variables locales de la función en cuanto se pasa el control dentro de la función. Sin embargo, este no es el final de su creación. A continuación, se inicializan (se establecen los valores iniciales), siendo la inicialización definida explícitamente por el programador o implícitamente por los valores por defecto del compilador. Al mismo tiempo, es esencial el contexto en el que se describen las variables.