变量的上下文、作用域和生存期

MQL5 是一种使用花括号将语句分组为代码块的编程语言。

回顾一下,一个程序由包含语句的代码块组成,其中必须明确存在一个代码块。在第一章的脚本示例中,我们看到了 OnStart 函数。这个函数的主体(函数名后面花括号里的文本)就是这样一个必要的代码块。

在每个代码块内部形成了局部上下文,也就是一个区域,用于限制其中描述的变量的可见性和生存期。到目前为止,我们只遇到了用花括号定义函数主体的例子。但是,花括号也可以用于构成 复合运算符,在用于 描述类命名空间的语法中使用。所有这些方法还定义了可见性区域,将在相关章节中讨论这些方法。在此阶段,我们只探讨一种类型的局部代码块,即函数内部的局部代码块。

除了局部区域之外,每个程序也有一个全局上下文,在这个区域中,变量、函数和其他实体的定义不受其他代码块的约束。

在简单的脚本端,MQL 向导创建了唯一的 void 函数 OnStart,那么该函数内部只有 2 个区域:一个全局区域和一个局部区域(局部区域在 OnStart 函数体的内部,尽管是空的)。以下脚本通过注释说明了这一点。

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

请注意,除了不涵盖 OnStart 函数,全局区域可以扩展到任何地方(包括 OnStart 函数前后)。全局区域基本涵盖了函数除外的所有内容(如果有多个函数的话),但是在这个脚本中,除了 OnStart 之外没有任何内容。

我们可以在文件的顶部描述变量,如 i, j, k,这些变量会成为全局变量。

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

全局变量在终端中启动 MQL 程序后立即创建,并且在整个程序执行期间都存在。

程序员可以在程序的任何地方记录和读取全局变量的内容。

原则上,建议只在文件顶部描述全局变量,但这并非强制要求。如果我们将声明移到 OnStart 函数整体的下方,本质上没有任何变化。只不过其他程序员很难立即就能弄清楚包含变量的代码的含义,因为他们仍然需要了解变量的定义。

有趣的是,OnStart 函数本身也是在全局上下文中声明的。如果我们添加另一个函数,也将在全局上下文中声明。回想一下我们在第一章中是如何创建 Greeting 函数,以及如何从 OnStart 函数调用 Greeting 函数。这就是因为函数名以及引用方法(即如何执行)在源代码中具有全局可见性。 命名空间 给这一机制增加了一些细节;我们将在后面学习这些知识。

每个函数内部的局部区域只属于函数本身:一个局部区域在 OnStart 内部,另一个在 Greeting 内部,这是该函数本身的区域,既不同于 OnStart 的局部区域,也不同于全局区域。

函数主体中描述的变量称为局部变量。局部变量是根据程序执行期间调用相关函数时的说明而创建的。局部变量只能在包含它们的代码块的内部使用。在外部不可见,也无法从外部访问。退出函数时,局部变量被销毁。

描述 OnStart 函数中 x, y, z 局部变量的示例:

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

应该注意,成对的花括号既可以用于说明函数和其他语句,它本身也可以用来构成内部代码块。单元嵌套没有限制。

通常添加嵌套代码块是为了最大程度上缩小逻辑隔离的小代码段中使用的变量的作用域(由于某些原因没有被函数设置为隔离)。这可以降低以下风险:在没有提供变量的情况下错误修改变量;或者由于试图将同一变量重用于各种需求(这种做法并不可取)而导致的意外副作用。

在下面这个示例函数中,单元嵌套级别为 2(如果我们将包含函数主体的代码块视为第一级),并且创建了 2 个这样的代码块并将连续执行。

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
}

在这两个代码块内部,都描述了变量 p,该变量在这两个代码块中用途不同。事实上这是两个不同的变量,尽管在每个代码块内部显示了相同的名称。

如果变量移动到函数的局部变量的初始列表中,那么在第一个代码块退出后,这个变量可能还包含一些残存值,从而中断第二个代码块的运行。此外,程序员偶尔会将 p 包含在函数一开头的某些其他元素中,那么副作用在第一个代码块就会出现。

在两个嵌套代码块之外,变量 p 是未知的,因此,试图从函数的公共代码块中引用该变量会导致编译错误(“未声明的标识符”)。

还应该注意的是,可以不在代码块最开始的位置描述变量,而是在代码块的中间或者靠近结尾处描述。这样,变量的作用域不是整个代码块,而是仅从定义的位置向下延伸。因此,在该变量的描述位置之前引用该变量也会出现同样的错误。

因此,变量作用域不同于上下文(整个代码块)。

下面的示例说明这个问题的两种情况:尝试使用语句 p = xy = p 包含任何字符串,并编译源代码。

一旦在函数内部传递了控制权,就为函数的所有局部变量分配内存。然而,这并不是变量创建的终点。还要对变量进行初始化(设置初始值),初始化可由程序员显式定义,也可以由编译器的默认值隐式定义。同时,描述变量的上下文也很重要。