Appearance
变量
变量用于程序中的数据存储和管理。通过"变量"章节学习变量操作基础,包括声明、初始化和赋值方法。
在本章中,我们将学习在 MQL5 中使用变量的基本原理,也就是与内置数据类型相关的那些原理。具体来说,我们将考虑变量的声明和定义、根据上下文需求进行初始化的特点、生命周期,以及改变变量属性的基本修饰符。随后,基于这些知识,我们将使用新的自定义类型(联合类型、自定义枚举和别名)、类、指针和引用来扩展变量的功能。
MQL5 中的变量提供了一种存储各种类型数据的机制,在组织程序逻辑以及处理市场信息的操作中起着重要作用。本节包括以下子部分:
变量的声明和定义
变量声明是在程序中创建变量的步骤。在本部分,我们将了解如何声明和定义变量,以及如何指定它们的类型。
变量是一个已命名的内存单元,用于存储特定类型的数据。为了使程序能够操作一个变量,程序员必须在源代码中声明和/或定义它。一般来说,对于程序元素,“声明”和“定义”这两个术语有着不同的含义,不过对于变量而言,它们实际上几乎总是一致的。当我们学习函数、类和特殊(外部)变量时,会涉及到这些复杂的内容。在这里,我们将互换使用这两个术语,同时使用“描述”这个更具概括性的词。
可以稳妥地认为,声明包含对一个程序元素的描述,其中涵盖了在程序中使用该元素所需的所有属性。然而,定义则包含了这个元素与声明相对应的具体实现。
声明使编译器能够将程序的所有元素相互关联起来。编译器根据定义生成可执行代码。
对于变量来说,它们的声明实际上总是充当其定义,因为声明确保了内存的分配,并根据变量的类型解释其内容(这正是变量的一种实现)。唯一的例外是使用 extern
关键字声明的变量(更多详细信息,请参阅“外部变量”部分)。
只有在对变量进行描述之后,你才能使用特殊的语句向其输入值、读取值,并通过引用变量名将其从程序的一个部分传递到另一个部分。
在最简单的情况下,描述变量的语句形式如下:
类型 变量名;
在这里,“变量名”必须符合标识符的构造要求。作为“类型”,你可以指定我们在上一部分中讨论过的任何一种内置类型,或者其他一些自定义类型——我们稍后将学习如何创建它们。例如,声明整数变量 i
如下:
c
int i;
如果需要,你可以同时描述几个相同类型的变量。在这种情况下,它们的名称在语句中指定,并用逗号分隔。
c
int i, j, k;
一个重要的因素是包含变量描述的语句在程序中所处的位置。这会影响变量的生命周期以及它在程序不同部分的可访问性。
变量的上下文、作用域和生命周期
变量可以存在于不同的上下文和作用域中,这会影响它们的可用性和生命周期。本小节将涵盖这些方面,帮助你理解变量如何与你的代码进行交互。
MQL5 属于使用花括号将语句分组为代码块的编程语言。
回想一下,一个程序由包含语句的代码块组成,并且肯定会存在一个代码块。在第 1 部分的脚本示例中,我们看到了 OnStart
函数。这个函数的主体(函数名后面用花括号括起来的文本)正是这样一个必需的代码块。
在每个代码块内部,会形成局部上下文,也就是一个限制在其中所描述变量的可见性和生命周期的区域。到目前为止,我们只遇到过花括号定义函数主体的示例。然而,花括号也可以用于形成复合运算符,以及在类和命名空间的描述语法中使用。所有这些方式也都定义了可见性区域,我们将在相关部分进行讨论。在这个阶段,我们只考虑一种局部代码块,即函数内部的代码块。
除了局部区域,每个程序还存在一个全局上下文,也就是一个包含在其他代码块之外定义的变量、函数和其他实体的区域。
对于简单的脚本,如果 MQL 向导创建了唯一的 void
函数 OnStart
,那么其中将只有两个区域:一个全局区域和一个局部区域(在 OnStart
函数主体内部,尽管它可能为空)。下面的脚本通过注释对此进行了说明:
c
// 全局作用域
void OnStart()
{
// "OnStart" 函数的局部作用域
}
// 全局作用域
请注意,全局区域除了 OnStart
函数内部之外无处不在(在函数之前和之后)。基本上,它包含了所有函数之外的部分(如果有多个函数的话),但在这个脚本中,除了 OnStart
函数之外没有其他内容。
我们可以在文件顶部描述变量,比如 i
、j
、k
,这样它们就会成为全局变量。
c
// 全局作用域
int i, j, k;
void OnStart()
{
// "OnStart" 函数的局部作用域
}
// 全局作用域
全局变量在终端中启动 MQL 程序时会立即创建,并在程序执行的整个期间都存在。
程序员可以从程序中的任何位置记录和读取全局变量的内容。
基本上建议将全局变量描述在文件顶部,但这并不是必需的。如果我们将声明移到整个 OnStart
函数的下方,基本上也不会有什么变化。只是其他程序员可能会很难立即理解带有变量的代码,因为他们还需要去查找变量的定义。
有趣的是,OnStart
函数本身也是在全局上下文中声明的。如果我们添加另一个函数,它也会在全局上下文中声明。回想一下我们在第 1 部分中是如何创建 Greeting
函数并从 OnStart
函数中调用它的。这就是函数名以及引用它的方法(如何执行它)在整个源代码中都已知的效果。命名空间会为其增加一些细节;不过,我们稍后会学习它们。
每个函数内部的局部区域只属于该函数:OnStart
函数内部有一个局部区域,Greeting
函数内部有另一个局部区域,它是独立的,与 OnStart
函数的局部区域和全局区域都不同。
在函数主体中描述的变量称为局部变量。它们是在程序执行期间调用相关函数时,根据其描述创建的。局部变量只能在包含它们的代码块内部使用。从外部来看,它们是不可见且不可访问的。当离开函数时,局部变量会被销毁。
在 OnStart
函数内部描述局部变量 x
、y
、z
的示例:
c
// 全局作用域
int i, j, k;
void OnStart()
{
// "OnStart" 函数的局部作用域
int x, y, z;
}
// 全局作用域
应该注意的是,花括号对既可以用于描述函数和其他语句,也可以单独用于形成内部代码块。嵌套的层数是没有限制的。
通常添加嵌套代码块是为了最小化在逻辑上独立的小代码位置中使用的变量的作用域(如果由于某种原因函数没有设置合适的作用域)。这可以降低在未预期的情况下错误修改变量的概率,或者避免由于试图将同一个变量用于不同需求而产生的一些不期望的副作用(这不是一个好的编程实践)。
下面是一个示例函数,其中嵌套层数为 2(如果我们将函数主体所在的代码块视为第一层),并且创建了 2 个这样的代码块并将依次执行。
c
void OnStart()
{
// "OnStart" 函数的局部作用域
int x, y, z;
{
// 局部子作用域 1
int p;
// ... 使用 p 完成任务 1
}
{
// 局部子作用域 2
// y = p; // 错误:'p' 未声明的标识符
int p; // 从现在起 'p' 已声明
// ... 使用 p 完成任务 2
}
// p = x; // 错误:'p' 未声明的标识符
}
在两个代码块内部都描述了变量 p
,并在其中用于不同的目的。实际上,这是两个不同的变量,尽管在每个代码块内部它们具有相同的名称。
如果将该变量移到函数的局部变量初始列表中,那么在从第一个代码块退出时,它可能会包含一些残留的值,从而破坏第二个代码块的操作。此外,程序员可能会在函数一开始就偶然地将 p
用于其他用途,然后在第一个代码块中就可能会产生副作用。
在这两个嵌套代码块之外,变量 p
是未知的,因此,从函数的公共代码块中尝试引用它会导致编译错误(“未声明的标识符”)。
还应该注意的是,变量不一定在代码块的开头进行描述,也可以在中间甚至接近结尾的位置进行描述。那么它不是在整个代码块中都被定义,而只是在其定义的下方有效。因此,在变量描述的上方引用它时,会出现同样的错误。
因此,变量的作用域区域可能与上下文(整个代码块)不同。
这两个问题的情况都在一个示例中进行了说明:尝试包含带有 p = x
和 y = p
语句的任何一行代码并编译源代码。
一旦控制权进入函数内部,就会为函数的所有局部变量分配内存。然而,这并不是它们创建过程的结束。然后它们会被初始化(设置初始值),初始化可以由程序员显式定义,也可以由编译器的默认值隐式定义。同时,描述变量的上下文至关重要。
初始化
变量的初始化涉及为它们分配初始值。我们将研究初始化的方法,有助于避免程序出现未定义的行为。 在描述变量时,可以设置初始值;初始值在变量名和符号“=”之后指定,并且必须与变量类型相符,或者能转换为该类型(类型转换可在相关部分找到)。
c
int i = 3, j, k = 10;
在这里,i
和 k
被显式初始化,而 j
没有被初始化。
既可以将常量(相关类型的字面量),也可以将表达式(一种用于计算的公式)指定为初始值。我们将单独阐述表达式。与此同时,看一个简单的例子:
c
int i = 3, j = i, k = i + j;
在这里,变量 j
取与变量 i
相同的值,而变量 k
取 i
和 j
的和。严格来说,在这三种情况下,我们看到的都是表达式。然而,常量(3)是一种特殊的、简化的表达式形式。在第二种情况下,唯一的变量名就是一个表达式,即表达式的结果将是这个变量的值,无需任何转换。在第三种情况下,表达式中访问了两个变量 i
和 j
,对它们的值执行加法运算,然后结果存储到变量 k
中。
由于包含多个变量描述的语句是从左到右处理的,所以编译器在分析另一个变量描述时,已经知道了前面变量的名称。
一个程序通常包含许多描述变量的语句。编译器会按自然的从上到下的顺序读取这些语句。在后面的初始化中,可以使用前面描述中定义的变量名。下面是通过两条单独的语句描述的相同变量。
c
int i = 3, j = i;
int k = i + j;
没有显式初始化的变量也会得到一些初始值,但这些值取决于变量被描述的位置,即它的上下文。
在没有初始化的情况下,局部变量在生成时会取随机值:编译器只是根据类型大小为它们分配内存,而特定地址上会存储什么是未知的(计算机的各种内存区域在不再被先前执行的程序使用后,常常会被重新分配以用于不同的程序)。
通常建议在算法代码的某个地方,通过赋值操作(我们稍后会讨论)向未初始化的局部变量中输入有效工作值。从语法上讲,这与初始化类似,因为它也使用等号“=”将值从位于其右边的“结构”(可以是常量、变量、表达式或函数调用)传输到左边的变量中。“=”左边只能是变量。
程序员应该确保只有在为未初始化的变量赋值后,才从该变量中读取值。如果不是这样,编译器会给出警告(“可能使用了未初始化的变量”)。
全局变量的情况则完全不同。
第 2 部分中 GoodTime2
脚本的输入参数 GreetingHour
就是全局变量的一个例子。用关键字 input
描述变量这一事实,并不影响它作为变量的其他属性。我们可以排除它的初始化,并如下描述它:
c
input uint GreetingHour;
这在程序中不会改变任何东西,因为如果没有显式初始化,编译器会使用零对全局变量进行隐式初始化(而之前我们也用零进行了显式初始化)。
无论变量类型是什么,隐式初始化总是使用等效于零的值来进行。例如,对于 bool
类型的变量,会设置为 false
,而对于 datetime
类型的变量,会设置为 D'1970.01.01 00:00:00'
。对于字符串,有一个特殊的值 NULL
。可以说,它是一个比空引号 ""
更“空”的字符串,因为空引号仍然会分配一些内存,其中只放置了一个终端空字符。
除了局部变量和全局变量,还有另一种类型,即静态变量。如果程序员没有编写显式的初始值,编译器也会用零对它们进行隐式初始化。我们将在下一部分讨论它们。
让我们创建一个新的脚本 VariableScopes.mq5
,其中包含描述局部变量和全局变量的示例(MQL5/Scripts/MQL5Book/VariableScopes.mq5
)。
c
// 全局变量
int i, j, k; // 全部为 0
int m = 1; // m = 1 (在此行设置断点)
int n = i + m; // n = 1
void OnStart()
{
// 局部变量
int x, y, z;
int k = m; // 警告:'k' 的声明隐藏了全局变量
int j = j; // 警告:'j' 的声明隐藏了全局变量
// 在赋值语句中使用变量
x = n; // 可以,值为 1
z = y; // 警告:可能使用了未初始化的变量 'y'
j = 10; // 改变局部的 j,全局的 j 仍然为 0
}
// 编译错误
// int bad = x; // 'x' - 未声明的标识符
应该记住,在启动 MQL 程序时,终端首先会初始化所有全局变量,然后调用一个函数,该函数是相关类型程序的起点。在这种情况下,对于脚本来说就是 OnStart
函数。
在这里,只有变量 i
、j
、k
、m
、n
是全局变量,因为它们是在函数外部描述的(在我们的例子中,我们只有一个函数 OnStart
,这对于脚本来说是必需的)。i
、j
、k
隐式地取值为 0。m
和 n
的值为 1。
你可以在调试模式下一步一步地运行脚本,并确保变量的值确实以这种方式变化。为此,你应该预先在其中一个全局变量(如 m
)的初始化字符串上设置一个断点。将文本光标放在该行上,然后执行“调试 -> 切换断点”(F9),该行将在左侧区域用蓝色标志突出显示,这表示如果程序在调试器上开始运行,执行将在此处停止。
然后,你实际上应该运行程序进行调试,为此执行命令“调试 -> 在真实数据上启动”(F5)。此时,终端中将打开一个新图表,在此图表中开始执行此脚本(右上角的标题为“VariableScopes (Debugging)”),但它会立即暂停,然后我们回到 MetaEditor。我们应该在其中看到如下画面。
在 MetaEditor 中逐步调试和查看变量
包含断点的行现在用箭头标志标记——这是程序准备执行但尚未执行的当前语句。程序的当前堆栈显示在左下角,到目前为止它只包含一个条目:@global_initializations
。你可以在右下角输入表达式以监控它们的实时值。我们对变量的值感兴趣;因此,让我们依次输入 i
、j
、k
、m
、n
、x
、y
、z
(每个变量占单独一行)。
你会进一步看到,MetaEditor 会自动添加来自当前上下文的变量以供查看(例如,局部变量以及在函数内部执行语句时的函数输入)。但现在,我们要提前手动添加 x
、y
和 z
,只是为了表明它们在函数外部未定义。
请注意,对于局部变量,显示的不是值,而是“未知标识符”,因为尚未进入 OnStart
函数块,而这些局部变量位于该函数块内。全局变量 i
和 j
最初的值将为零。全局变量 k
未在任何地方使用,因此它被编译器排除。
如果我们使用“单步进入”(F11)或“单步跳过”(F10)命令执行程序的一步(执行当前代码行上的语句),我们将看到变量 m
如何取值为 1。再执行一步将继续对变量 n
进行初始化,它也将变为 1。
在这里,全局变量的描述结束,并且正如我们所知,终端在完成全局变量的初始化后会调用 OnStart
函数。在这种情况下,要以逐步模式进入 OnStart
函数,再次按下 F11(或者你可以在 OnStart
函数的开头设置另一个断点)。
局部变量在程序语句的执行到达定义它们的代码块时进行初始化。因此,变量 x
、y
、z
只有在进入 OnStart
函数时才会被创建。
当调试器进入 OnStart
函数内部时,如果运气好的话,你将能够看到 x
、y
和 z
中确实最初是随机值。这里的“运气”在于这些随机值很可能是零值。然后就无法将它们与编译器为全局变量执行的隐式零初始化区分开来。如果反复启动脚本,局部变量中的“垃圾值”可能会不同,并且更具说明性。它们没有被显式初始化,因此其内容可能是任何类型。
在下面的一系列图像中,你可以通过调试器的逐步模式看到变量的变化情况。要执行但尚未执行的当前行在带有编号的区域中用绿色箭头标记。
代码中进一步展示了如何以最简单的方式在赋值运算符中使用这些变量。全局变量 n
的值可以毫无问题地复制到局部变量 x
中,因为 n
已经被初始化。然而,在将变量 y
的内容复制到变量 z
的这一行中,会出现编译器的警告,因为 y
是局部变量,并且到目前为止没有向其中写入任何内容;也就是说,没有显式初始化,也没有其他可以设置其值的操作。
在函数内部,允许描述与已用于全局变量的名称相同的变量。如果在内部块中创建了一个与外部块中已存在的名称相同的变量,在嵌套的局部块中也可能会出现类似的情况。然而,不建议这样做,因为这可能会导致逻辑错误。在这种情况下,编译器会给出警告(“声明隐藏了全局/局部变量”)。
由于这种重新定义,像上面示例中的局部变量 k
在函数内部会覆盖同名的全局变量。尽管它们具有相同的名称,但这是两个不同的变量。局部的 k
在 OnStart
函数内部可见,而全局的 k
在除 OnStart
函数之外的任何地方可见。换句话说,在块内对变量 k
进行的任何操作只会影响局部变量。因此,在退出 OnStart
函数时(就好像它不是脚本的唯一核心函数一样),我们会发现全局变量 k
仍然等于零。
局部变量 j
不仅覆盖了全局变量 j
,而且还通过后者的值进行初始化。在 OnStart
函数内部包含 j
描述的这一行中,当从全局版本的 j
中读取初始值时,局部版本的 j
仍在创建中。在成功定义局部的 j
后,这个名称覆盖了全局版本,并且后续对 j
的更改都属于局部版本。
在源代码的末尾,我们注释掉了尝试声明另一个全局变量 bad
的操作,在其初始化中调用了变量 x
的值。这一行会导致编译器错误,因为变量 x
在定义它的 OnStart
函数之外是未知的。
静态变量
静态变量在函数调用之间保留它们的值。本节将解释如何使用静态变量在不同的代码执行之间存储信息。
有时需要在函数内部描述一个变量,并确保它在程序执行的整个过程中都存在。例如,我们想要统计这个函数被调用了多少次。
这样的变量不能是局部变量,因为那样它就会失去“长期记忆”,因为每次调用函数时它都会被创建,而在退出函数时又会被删除。从技术上讲,它可以被描述为全局变量;然而,如果这个变量只在这个函数中使用,从程序设计的角度来看,这种方法是错误的。
首先,全局变量可能会在程序的任何地方被意外修改。
其次,想象一下,如果我们稍有借口就声明一个全局变量,程序的全局区域将会变成一个多么混乱的变量“动物园”。相反,建议在使用变量的最小代码块中(如果有多个嵌套代码块)声明变量。
因此,函数执行次数的计数器应该在函数内部进行描述。这就是变量的一个新属性——静态属性发挥作用的地方。
在变量声明时,在变量类型前面放置一个特殊的关键字(修饰符)static
,可以将其生命周期延长到程序执行的整个过程,也就是说,使它类似于全局变量。通常,静态变量只在局部定义,即在其中一个函数内。因此,它的可见性像普通局部变量一样,受到相关代码块的限制。
静态变量也可以在全局级别进行描述,但(至少在撰写本书时)它与普通全局变量没有任何区别。这与它们在 C++ 中的行为不同:在 C++ 中,它们的可见性受到其所在文件的限制。在 MQL5 中,一个程序是基于一个主 mq5 文件,可能还会有一些头文件(见 #include
指令)来组装的;因此,静态全局变量和普通全局变量在程序的所有源文件中都是可用的。
局部静态变量只创建一次——在程序第一次进入描述该变量的函数时创建。这样的变量只有在卸载程序时才会被删除。如果一个函数从未被调用,那么在其中描述的局部静态变量(如果有的话)将永远不会被创建。
例如,让我们修改第 1 部分中的 Greeting
函数,使它在每次调用时给出不同的问候语。我们将新脚本命名为 GoodTimes.mq5
。
我们将删除脚本 GreetingHour
的输入以及 Greeting
函数的参数。在 Greeting
函数内部,我们将描述一个新的整型静态变量 counter
,初始值为 0。需要提醒的是,这是初始化操作,并且由于变量是静态的,它只会执行一次。
c
string Greeting()
{
static int counter = 0;
static string messages[3] =
{
"Good morning", "Good day", "Good evening"
};
return messages[counter++ % 3];
}
既然我们现在了解了 static
修饰符,那么对数组 messages
也使用它是合理的。问题在于,它之前被声明为局部变量,每次调用 Greeting
函数时它都会被重新创建(并且在退出时被删除)。这是低效的。
需要提醒的是,数组是一组具有相同类型的多个值的命名集合,可以通过在名称后面的方括号中指定的索引来访问。关于变量的很多内容都直接适用于数组。使用数组的更多细节将在“数组”部分介绍。
但让我们回到当前的问题。在 return
语句中,根据 counter
变量的值从数组中选择一个选项,目前看起来有点神秘:
c
return messages[counter++ % 3];
我们在第 1 部分中已经顺便提到了使用字符 %
进行的取模运算。通过它,我们保证元素索引不会超过数组的大小:无论 counter
是多少,它除以 3 的余数只能是 0、1 或 2。
同样适用于 counter++
这个结构,它表示将变量的值加 1(单步递增)。
重要的是要注意,在这种表示法中,递增操作将在计算完整个表达式之后进行,在这种情况下,是在计算 counter % 3
之后。这意味着计数将从初始值 0 开始。也有可能在计算表达式之前进行递增操作,写成 ++counter % 3
。那样的话,计数将从 1 开始。我们将在“递增和递减”部分讨论这种类型的操作。
让我们从 OnStart
函数中连续 3 次调用 Greeting
函数。
c
void OnStart()
{
Print(Greeting(), ", ", Symbol());
Print(Greeting(), ", ", Symbol());
Print(Greeting(), ", ", Symbol());
// Print(counter); // 错误:'counter' - 未声明的标识符
}
结果,我们将在日志中看到预期的三行字符串,依次显示所有的问候语。
GoodTimes (EURUSD,H1) Good morning, EURUSD
GoodTimes (EURUSD,H1) Good afternoon, EURUSD
GoodTimes (EURUSD,H1) Good evening, EURUSD
如果我们继续调用该函数,counter
将会增加,并且问候语将会循环显示。
在 OnStart
函数末尾尝试引用 counter
变量(已注释)将导致代码无法编译,因为静态变量虽然继续存在,但只在 Greeting
函数内部可用。
请注意,花括号既用于形成代码块,也用于初始化数组。你应该区分它们的不同应用。数组将在相关部分详细介绍。然而,这并不是花括号的所有应用:通过它们,我们稍后将学习如何定义自定义类型、结构体和类。静态变量也可以在结构体和类内部定义。
常量变量
常量变量表示在程序执行期间不会改变的值。本节将描述它们的用法和特点。 尽管这看起来有些矛盾,但大多数编程语言都支持常量变量的概念。在 MQL5 中,通过添加修饰符 const
来描述常量变量。它被放在变量描述中,位于变量类型之前,表示在使用初始值对变量进行初始化后,其值不能以任何方式被更改。在变量的整个生命周期内,它将保持相同的值,即一个常量。
编译器会阻止对常量进行赋值操作:在相关代码行中会出现“常量不能被修改”的错误。
修饰符 const
的目的是明确表明程序员不打算更改相关变量的意图,比如在使用一些众所周知的固定值时,如计算美元指数的欧元指数、一年中的周数等等。如果你不打算更改一个变量,建议始终使用 const
修饰符。这有助于避免以后可能出现的错误,因为如果程序员自己或同事不小心试图向常量中写入其他内容,使用 const
修饰符可以防止这种情况发生。
例如,我们可以在 Greeting
函数中为 messages
数组添加 const
修饰符。对于这样一个小程序来说,这似乎并没有明显的用处。然而,由于程序往往会不断扩展,任何代码行迟早都可能会处于一个更加复杂的软件环境中,比如添加了新的语句、操作模式等等。因此,制定一个备用方案是有意义的;特别是当这很容易做到的时候。
c
string Greeting()
{
static int counter = 0;
static const string messages[3] =
{
"Good morning", "Good day", "Good evening"
};
// 错误演示:'messages' - 常量不能被修改
// messages[0] = "Good night";
return messages[counter++ % 3];
}
在注释的代码行中,我们尝试将字符串“Good night”写入数组的第一个元素(记住数组编号从 0 开始)。在这种情况下,这样做的目的只是为了确认编译器会阻止这种操作。
很容易看出,修饰符 static
和 const
可以组合使用。它们的书写顺序并不重要。
顺便说一下,在 MQL5 中,使用修饰符 const
或者将变量声明为程序的输入变量时,变量都会成为常量。
输入变量
输入变量用于交易机器人中配置策略参数。我们将了解如何使用它们来创建灵活且可定制的交易系统。 在启动时,MQL5 中的所有程序都可以向用户询问参数。唯一的例外是库,库不会独立执行,而是作为其他程序的一部分(更多关于库的内容可查看相关章节)。
MQL 程序的输入参数是在代码中被描述为带有特殊修饰符 input
或 sinput
的全局变量。这些变量会在程序属性对话框中供用户输入值。在第 1 部分的脚本中,我们看到过 GreetingHour
输入变量的描述。
输入变量的一个特性是,其值不能在程序代码中被更改,也就是说,它表现得像一个常量。
输入变量只能是简单的内置类型或枚举类型。对于枚举类型,用户可以通过下拉列表输入值;在其他情况下,则使用输入字段。不允许将数组、结构体、联合体和类描述为输入变量。
开发者可以为输入参数设置不同于变量标识符的名称。这个名称会在程序属性对话框中显示给用户。在定义输入参数时,应该添加单行注释作为详细描述。
c
input int HourStart = 0; // 交易开始时间(小时,包含该小时)
input int HourStop = 0; // 交易结束时间(小时,不包含该小时)
这样做可以使界面更友好、更详细,并且不受 MQL5 对标识符施加的语法限制。此外,名称(以及注释)可以使用你自己的母语。
例如,MetaTrader 5 自带的指标 MQL5/Indicators/Examples/Custom Moving Average.mq5
的源代码中包含以下输入变量:
c
input int InpMAPeriod = 13; // 周期
input int InpMAShift = 0; // 偏移
input ENUM_MA_METHOD InpMAMethod = MODE_SMMA; // 方法
这段描述会生成如下的属性对话框。
输入变量以标识符 = 值的形式表示时,包括“=”字符,其文本表示的最大长度不能超过 255 个字符(这一限制是由终端和测试代理的内部数据交换协议施加的)。这个限制对于字符串变量尤为重要,因为其他类型的值通常不会超过这个长度。我们知道,标识符的长度限制为 63 个字符;因此,根据标识符的长度,输入字符串变量的值最多还可以有 191 - 253 个字符。当传输到测试器时,超过 255 字符的整个文本可能会被截断。如果需要向 MQL 程序中输入更长的字符串,可以使用多个输入字段(后续会介绍),或者允许用户指定一个文件的名称,从该文件中读取文本。
为了方便操作 MQL 程序,可以使用关键字 group
将输入参数组合到命名块中(组行末尾不需要分号)。
c
input group "group_name"
input type identifier = value;
...
在组描述之后(直到另一个组的描述或文件末尾)带有 input
修饰符的所有变量,会在 MQL 程序的属性对话框中以嵌套列表的形式显示在组标题下。此外,在适用于指标和智能交易系统的策略测试器中,可以通过鼠标点击展开或折叠参数组。
关键字 sinput
是 static input
的缩写,这两种形式是等价的。
使用 sinput
和 static input
修饰符描述的变量不能参与优化。只有在支持优化的唯一 MQL 程序类型——智能交易系统中使用它们才有意义。更多详细信息,请参阅“测试和优化智能交易系统”部分。
外部变量
外部变量允许用户与程序进行交互,因为它们的值可以在无需修改代码的情况下进行更改。本节将解释外部变量的工作原理。
本节的内容既复杂又具有可选性。它需要对基于与 C++ 类比的概念以及后续要介绍的概念有所了解。同时,所描述的语言结构的效果可以通过其他方式实现,而且其灵活性可能会成为错误的潜在来源。
MQL5 允许将变量描述为外部变量。这通过使用 extern
关键字来实现,并且只允许在全局上下文中使用。
对于外部变量,其语法基本上与普通变量描述相同,但它额外带有 extern
关键字,并且禁止进行初始化:
c
extern 类型 标识符;
将变量描述为外部变量意味着其描述被延迟,并且必须在源代码的后续部分出现,通常是在另一个文件中(使用 #include
指令连接文件将在处理预处理器的章节中讨论)。几个不同的源文件可以对同一个外部变量进行描述,即那些具有相同类型和标识符的变量。所有这些描述都指向同一个变量。
一般认为,这个变量将在其中一个文件中被完整描述。如果在代码中任何地方都没有在没有 extern
关键字的情况下定义该变量,将会返回“未解析的外部变量”编译错误(在这种情况下类似于 C++ 中的链接器错误)。
描述外部变量可以在特定文件的源代码中有效地使用它。换句话说,即使该变量并非在这个模块中创建,它也能使给定模块得以编译。
在 MQL5 中使用 extern
不像在 C++ 中那样必要,在大多数情况下,可以通过包含一个头文件来替代,该头文件对要声明为 extern
的变量进行了通用描述。常规地执行这些定义就足够了。编译器会确保每个附加的文件在源代码中只被添加一次。考虑到在 MQL5 中一个程序总是由一个可编译单元 mq5 组成,这里不存在 C++ 中由于在不同单元中包含头文件而可能导致的同一变量多重定义的错误问题。
即使在 #include
指令中附加了一个额外的 mq5(而不是 mqh)文件,它也不会与启动编译的主单元处于同等竞争地位;相反,它会被视为头文件之一。
与 C++ 不同,MQL5 不允许为外部变量指定初始值(在 C++ 中进行初始化会导致忽略 extern
关键字)。如果你尝试设置初始值,将会得到编译错误“不允许对外部变量进行初始化”。
一般来说,将变量描述为外部变量可以被视为一种“软”描述:它确保了变量的存在,并避免了如果在多个文件中没有 extern
修饰符描述变量而可能出现的覆盖错误。
然而,这也可能成为错误的来源。如果在不同的头文件中,偶然地为不同目的描述了相同的变量,那么没有 extern
关键字将无法识别冲突,而有了 extern
关键字,这些变量将被视为同一个,并且程序的操作逻辑很可能会被破坏。
既可以将变量描述为外部变量,也可以将函数描述为外部函数(下面将讨论函数的情况)。对于函数,将其描述为外部函数是一种遗留形式(即它可以被编译,但不会产生任何实际变化)。以下两种函数声明是等价的:
c
extern 返回类型 函数名([参数]);
返回类型 函数名([参数]);
从这个意义上说,extern
的有无只能在风格上用于区分当前单元的函数前置描述(没有 extern
)和外部单元的函数前置描述(有 extern
)。
你可以在要编译的 mq5 单元以及要附加的头文件中使用 extern
关键字。
让我们考虑一些使用 extern
的情况:它们被写在不同的文件中,即主脚本 ExternMain.mq5
和 3 个可附加文件:ExternHeader1.mqh
、ExternHeader2.mqh
和 ExternCommon.mqh
。
在主文件中,只附加了 ExternHeader1.mqh
和 ExternHeader2.mqh
,而稍后我们会用到 ExternCommon.mqh
。
c
// 来自 mqh 文件的源代码将被隐式替换
// 在主 mq5 文件中,替代这些指令
#include "ExternHeader1.mqh"
#include "ExternHeader2.mqh"
在头文件中,定义了两个有条件有用的函数:在第一个头文件 ExternHeader1.mqh
中,有用于对变量 x
进行递增的函数 inc
;在第二个头文件 ExternHeader2.mqh
中,有用于对变量 x
进行递减的函数 dec
。正是变量 x
在这两个文件中都被描述为外部变量:
c
// ExternHeader1.mqh
extern int x;
void inc()
{
x++;
}
// -----------------
// ExternHeader2.mqh
extern int x;
void dec()
{
x--;
}
由于这样的描述,每个 mqh 文件都可以正常编译。当它们一起被包含在一个 mq5 文件中时,整个程序也会被编译。
如果在每个文件中都没有 extern
关键字来定义变量,那么在编译整个程序时将会出现重定义错误。如果我们将 x
的定义从头文件转移到主单元中,头文件将无法再被编译(这对某些人来说可能不是问题;然而,在较大的程序中,开发人员喜欢在不编译整个项目的情况下检查即时修改的可编译性)。
在主脚本中,我们定义了一个变量(在这种情况下,初始值为 2,如果我们不指定值,则会使用默认值 0),并调用了这些有条件有用的函数,同时打印了 x
的值。
c
int x = 2;
void OnStart()
{
inc(); // 使用 x
dec(); // 使用 x
Print(x); // 2
...
}
在文件 ExternHeader1.mqh
中,有变量 short z
的描述(没有 extern
)。在主脚本中对类似的描述进行了注释。如果我们激活这一行,将会得到前面提到的错误(“变量已定义”)。这样做是为了说明潜在的问题。
在 ExternHeader1.mqh
中,也描述了 extern long y
。同时,在文件 ExternHeader2.mqh
中,同名的外部变量具有不同的类型:extern short y
。如果后面这个描述没有预先“移到”注释中,这里将会出现类型不兼容错误(“变量 'y' 已用不同类型定义”)。总结:要么类型必须一致,要么变量不能是外部变量。如果这两个选项都不合适,那就意味着其中一个变量的名称存在拼写错误。
此外,应该注意到变量 y
没有被显式初始化。然而,主脚本成功地调用了它,并在日志中打印了 0:
c
long y;
void OnStart()
{
...
Print(y); // 0
}
最后,脚本中提供了一种可能性,尝试以已经熟悉的变量 x
为例,使用外部孪生变量的替代方式。ExternHeader1.mqh
和 ExternHeader2.mqh
这两个文件不用描述 extern int x
,而是可以包含另一个公共头文件 ExternCommon.mqh
,在这个文件中有 int x
的描述(没有 extern
)。这样它就成为了项目中对 x
的唯一描述。
当激活宏 USE_INCLUDE_WORKAROUND
时,将启用这种替代的程序组装模式:它在脚本开头的注释中:
c
#define USE_INCLUDE_WORKAROUND // 这一行原本在注释中
#include "ExternHeader1.mqh"
#include "ExternHeader2.mqh"
在这种配置下,特定的包含文件仍然是可编译的,整个项目也是如此。在实际项目中,如果不使用这种方法,公共 mqh 文件将无条件地包含在 ExternHeader1.mqh
和 ExternHeader2.mqh
中(没有 USE_INCLUDE_WORKAROUND
条件)。在这个例子中,基于 USE_INCLUDE_WORKAROUND
在两种指令线程之间切换只是为了演示两种模式。例如,简化后的 ExternHeader2.mqh
应该如下所示:
c
// ExternHeader2.mqh
#include "ExternCommon.mqh" // int x; 现在在这里
void dec()
{
x--;
}
我们可以在 MetaEditor 日志中检查到,尽管 ExternCommon.mqh
在 ExternHeader1.mqh
和 ExternHeader2.mqh
中都被引用,但它只加载了一次。
'ExternMain.mq5'
'ExternHeader1.mqh'
'ExternCommon.mqh'
'ExternHeader2.mqh'
code generated
如果 x
变量在 ExternCommon.mqh
中“注册”了,我们就不应该在主单元中重新定义它(没有 extern
),因为这会导致编译错误,但我们可以在算法开头简单地为它赋上所需的值。