
掌握 MQL5:从入门到精通(第四部分):关于数组、函数和全局终端变量
概述
本文是初学者系列文章的延续。在前面的文章中,我们详细讨论了描述存储在程序中的数据的方法。至此,读者应该知道以下几点:
- 数据可以存储在变量或常量中。
- MQL5 语言是一种强类型语言,这意味着程序中的每条数据都有自己的类型,编译器使用该类型来正确分配内存并避免逻辑错误。
- 数据类型可以是简单的(基本的)也可以是复杂的(用户定义的)。
- 为了在 MQL5 程序中使用数据,您需要声明至少一个函数。
- 任何代码块都可以移动到单独的文件中,然后可以使用 #include 预处理器指令将这些文件包含在项目中。
在本文中,我将讨论三个全局性主题:
- 数据数组,完成程序内部关于数据的主要部分。
- 全局终端变量,允许在不同的 MQL5 程序之间交换简单数据。
- 函数的一些特性及其与变量的相互作用。
关于数组的基本信息
数组是包含一系列相同类型数据的变量。
要描述一个数组,您需要描述它的类型和变量名,然后写方括号。给定序列中的元素数量可以在方括号中指定。
int myArray[2]; // Describes an array of integers containing two elements
例 1 .静态数组的描述。
我们经常在 MQL5 程序中描述序列。这些序列包括历史价格、蜡烛图开启时间、交易量等。一般来说,只要有数据集,数组可能就是一个不错的选择。
MQL5 中数组内元素的编号始终从 0 开始。因此,数组中最后一个元素的数量始终等于其元素数量减一(lastElement = size - 1)。
要访问数组元素,只需在方括号中指定该元素的索引:
// Fill the array with values: myArray[0] = 3; myArray[1] = 315; // Output the last element of this array to the log: Print(myArray[1]); // 315
例 2 .使用数组元素。
当然,任何数组在声明时都可以使用花括号进行初始化,就像结构一样:
double anotherArray[2] = {4.0, 5.2}; // A two-element array is initialized with two values Print( DoubleToString(anotherArray[0],2) ); // Output 4.00
例 3 .描述时数组初始化。
多维数组
一个数组可以在其内部存储其他数组,这种嵌套数组被称为“多维”。
多维数组的一个简单的可视化示例可以是书籍页面。字符汇集为行,为第一维;行汇集为段落,为第二维;页面为段落的集合,为第三维。
图 01.字符汇集到一行中 — 一维数组。
图 2.行汇集到段落中 — 二维数组。
图 3.段落汇集到页面中 — 三维数组。
要在 MQL5 中描述此类数组,只需为每个新维度添加方括号。“外部”容器的括号位于“内部”容器的左侧。例如,图 1-3 所示的数组可以按如下方式描述和使用:
char stringArray[21]; // One-dimensional array char paragraphArray[2][22]; // Two-dimensional array char pageArray[3][2][22]; // Three-dimensional array // Filling a two-dimensional array paragraphArray[0][0]='T'; paragraphArray[0][1]='h'; // … paragraphArray[1][20]='n'; paragraphArray[1][21]='.'; // Access to an arbitrary element of a two-dimensional array Print(CharToString(paragraphArray[1][3])); // Will print "a" (why?)
例 4 .图1-3的多维数组描述。
数组的总维数不应超过 4。任意维度的最大元素数等于 2147483647。
多维数组的初始化与一维数组的初始化一样简单,花括号仅列出每个数组的元素:
int arrayToInitialize [2][5] = { {1,2,3,4,5}, {6,7,8,9,10} }
例 5.多维数组的初始化。
动态数组
并非所有数组都能立即知道它们有多少个元素。例如,包含终端历史记录或交易列表的数组会随着时间的推移而变化。因此,除了前面部分中描述的静态数组外,MQL5 还允许您创建动态数组,即那些可以在程序操作过程中更改其元素数量的数组。此类数组的描述方式与静态数组完全相同,只是元素的数量未在方括号中指明:
int dinamicArray [];
例6.动态数组的描述
以这种方式声明的新数组不包含任何元素,其长度为 0,因此无法访问其元素。如果程序试图这样做,将发生严重错误,程序将终止。因此,在使用这样的数组之前,必须使用特殊的内置函数 ArrayResize 设置其大小:
ArrayResize(dinamicArray, 1); // The first parameter is the array and the second is the new size ArrayResize(dinamicArray,1, 100); // The third parameter is the reserved (excess) size
例 7.调整动态数组的大小
在语言文档中,您可以看到该函数最多可以接受三个参数,但是第三个参数有一个默认值,因此您可以省略它,就像我在示例的第一行中所做的那样。
此函数的第一个参数必然是我们正在更改的数组。第二个是数组的新大小。我认为这不会有什么问题。第三个参数是“保留大小”。
如果我们知道数组的最终大小,则可以使用保留大小。例如,根据我们问题的条件,数组中不能有超过 100 个值,但确切有多少个值是未知的。那么,您可以在此函数中使用 reserve_size 参数并将其设置为 100,如第二行示例 7 所示。在这种情况下,尽管实际数组大小仍为第二个参数中指定的大小(1 个元素),但函数将为 100 个元素保留多余的内存大小。
为什么要搞这么复杂?为什么不在需要时一次添加一个元素?
简单的答案是加快我们的程序。
更详细的答案可能需要很长时间才能写出来。但简而言之,重点是每次我们使用不带第三个参数的 ArrayResize 函数时,我们的程序都会向操作系统请求额外的内存。这种内存分配是一个相当长的操作(从处理器的角度来看),内存是只需要一个元素还是一次需要多个元素并不重要。我们的程序执行这种操作的次数越少越好。也就是说,最好一次保留大量空间然后填充,而不是分配一点空间然后扩展。然而,在这里也有必要考虑到内存是一种有限的资源,因此您始终必须在操作速度和数据大小之间找到平衡。
如果你知道数组的局限性,最好通过声明静态数组或使用 ArrayResize 函数中的第三个参数保留内存来明确地向程序表明这一点。如果你不知道,那么数组肯定是动态的,ArrayResize 函数中的第三个参数不一定需要指定,尽管它可以,因为如果数组的实际大小大于保留的大小,MQL5 将简单地分配所需的实际内存。
调整数组大小后,如示例 7 所示,您可以修改其中的数据:
dinamicArray[0] = 3; // Now our array contains exactly one element (see example 7), its index is 0
例 8.使用修改后的数组
当我们使用动态数组时,最常见的任务是将数据添加到该数组的末尾,而不是在中间修改某些内容(尽管也会发生这种情况)。由于在任何给定的时刻,程序都不知道在该特定时刻数组中包含了多少元素,因此需要一个特殊的函数来找出。它被称为 ArraySize 。该函数接受一个参数(数组),并返回该数组中元素数量的整数值。一旦我们知道动态数组的确切大小(此函数将返回),添加元素就变得非常简单:
int size, // Number of elements in the array lastElemet; // Index of the last element char stringArray[]; // Our example dynamic array. // Immediately description its size is 0 (array cannot contain elements) // add an element to the end. size = ArraySize(stringArray); // Find the current size of the array size++; // Array size should increase by 1 ArrayResize(stringArray, size, 2); // Resize the array. In our example, the array will have no more than two elements. lastElemet = size — 1; // Numbering starts from 0, so the number of the last element is 1 less than the size of the array stringArray[lastElement] = `H`; // Write the value // Now add one more element. The sequence of actions is absolutely the same. size = ArraySize(stringArray); // Find the current size if the array size++; // Array size should increase by 1 ArrayResize(stringArray, size, 2); // Resize the array. In our example, the array will have no more than two elements. lastElemet = size — 1; // Numbering starts from 0, so the number of the last element is 1 less than the size of the array stringArray[lastElement] = `i`; // Write the value // Note that when adding the second element in this way, only on line changes: // the one that writes the actual value to a specific cell. // // This means that the solution can be written in a shorter form. For example, by creating a separate custom function for it.
例 9.将元素添加到动态数组的末尾。
使用动态数组时经常使用 ArraySize 和 ArrayResize 函数,通常以组合方式使用,如示例 9 所示。对于其他不太常用但同样有用的函数,请参阅文档。
并且,在本节的结论中,我想指出的是,MQL5 语言也支持多维动态数组,但是,其中只有第一个索引可以是未定义的。
int a [][12]; // It's ok // int b [][]; // Compilation error: only the first index can be dynamic
例 10.多维动态数组。
如果你真的需要使多个索引动态化,你可以创建一个结构,其唯一的字段将是一个动态数组,然后创建一个这样的结构数组。
struct DinArray // Structure containing a dynamic array { int a []; }; DinArray dinamicTwoDimensions []; // Dynamic array of structures ArrayResize( dinamicTwoDimensions, 1 ); // Set size of the outer dimension ArrayResize( dinamicTwoDimensions[0].a, 1 ); // Set size of the internal dimension dinamicTwoDimensions[0].a[0] = 12; // Use cell to write data
例 11.具有两个动态索引的数组。
还有其他方法可以解决这个问题。例如,您可以创建自己的类或使用标准库中已存在的类。但是,我将在以后的文章中讨论使用类的主题。
时间序列数组
MQL5 中的开盘价、收盘价、最高价和最低价、分时报价和实际交易量、价差、蜡烛图开始时间以及每根蜡烛图上的指标值被称为序列或时间序列。
MQL5 程序员无法直接访问这些时间序列,但该语言提供了使用一组特殊的预定义函数(如表 1 所列)将这些数据复制到程序内的任何变量中的能力。
如果我们需要收盘价,您首先需要创建自己的数组来存储这些价格,然后调用 CopyClose 函数并将创建的数组作为最后一个参数传递给它。该函数将标准时间序列复制到我们的变量中,然后可以以通常的方式使用这些数据:使用方括号中的索引。
然而,使用时间序列的操作与其他数组有些不同。这现在是一种传统的固定形式。
在内存中,时间序列的存储方式与所有其他数据相同:从最旧到最新。但是表 1 中用于序列数据操作的函数按照相反的顺序对时间序列中的元素进行编号,即从右到左。对于所有这些函数,零号烛形将是最右边的烛形,即当前的烛形,尚未完成的烛形。但“普通”数组不知道这一点,因此对于它们来说,这根烛形将是最后一根。这有点令人困惑……
让我们尝试使用下面的图片来理解这一点。
图 4.常规数组(绿色箭头)和时间序列(蓝色箭头)中的索引方向。
图 5.将序列复制到常规数组中。
图 4 显示了时间序列和常规数组的索引方向的差异。
图 5 直观地展示了如何将时间序列数据复制到常规数组中,例如,使用 CopyRates 或类似函数(见表 1)。 常规数组和时间序列的内存中元素的物理顺序相同,但索引会发生变化,并且复制后时间序列中的第一个元素将成为常规数组中的最后一个元素。
有时,在不断记住这些细微差别的同时,编程可能会很不方便。有两种方法可以消除这种不便:
- 标准的 ArraySetAsSeries 函数允许您更改任何动态数组的索引方向。它需要两个参数:数组本身和它是否是时间序列的指示(true/false)。如果您的算法涉及复制始终从最后一个烛形开始的数据,则通常可以将目标数组指定为一个序列,然后标准函数和您的算法将使用相同的索引来工作。
- 如果您的算法涉及从图表上的任意位置复制小数据片段,尤其是如果在算法的每个步骤中都准确知道它们的数量(例如,我们取三个柱形的收盘价:第一个关闭的柱形 - 即系列索引为 1 - 以及随后的两个柱形,系列索引为 2 和 3),那么最好按原样接受它。最好接受索引将在不同的方向上进行,并且在编程时要更加小心。一种可能的解决方案是创建一个单独的函数来检查所需的值,并尝试在任何表达式中使用它。
在下面的例子中,我试图用代码来说明上述所有内容。
datetime lastBarTime; // We'll try to write the last candle's time into this variable datetime lastTimeValues[]; // The array will store the time of the last two candles. // It's dynamic so that it can be made into a time series to test indices // Get the start time of the current candlestick using the iTime function lastBarTime = iTime ( Symbol(), // Use the current symbol PERIOD_CURRENT, // For the current timeframe 0 // Current candlestick ); Print("Start time of the 0 bar is ", lastBarTime); // Get the start time of the last two candlesticks using the CopyTime function CopyTime ( Symbol(), // Use the current symbol PERIOD_CURRENT, // For the current timeframe 0, // Start with position 0 2, // Take two values lastTimeValues // Write them to array lastTimeValues ("regular") array ); Print("No series"); ArrayPrint(lastTimeValues,_Digits,"; "); // Print the entire array to log. The separator between elements is a semicolon ArraySetAsSeries(lastTimeValues,true); // Convert the array into a time series Print("Series"); ArrayPrint(lastTimeValues,_Digits,"; "); // Print the entire array again. Note the order of the data /* Script output: 2024.08.01 09:43:27.000 PrintArraySeries (EURUSD,H4) Start time of the 0 bar is 2024.08.01 08:00:00 2024.08.01 09:43:27.051 PrintArraySeries (EURUSD,H4) No series 2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) 2024.08.01 04:00:00; 2024.08.01 08:00:00 2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) Series 2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) 2024.08.01 08:00:00; 2024.08.01 04:00:00 */
例 12.测试用于处理时间序列的函数。
表 1.访问时间序列的函数列表。对于所有这些函数,元素的索引都是从右侧开始的,从最后一根(未完成的)烛形开始。
函数 | 操作 |
---|---|
CopyBuffer | 将指定指标缓冲区的数据复制到数组中 |
CopyRates | 将指定交易品种和时段的历史数据放入 MqlRates 结构数组中 |
CopySeries | 获取指定数量的指定交易品种/时段的几个同步时间序列。所有需要填充的数组列表在最后传递,并且它们的顺序必须与 MqlRates 结构的字段相对应。 |
CopyTime | 将相应交易品种和时段的柱形开盘时间的历史数据放入数组中 |
CopyOpen | 将相应交易品种和时段的开盘价历史数据放入数组中 |
CopyHigh | 将相应交易品种和时段的最高价历史数据放入数组中 |
CopyLow | 将相应交易品种和时段的最低价历史数据并放入数组中 |
CopyClose | 将相应交易品种和时段的柱形收盘价历史数据放入数组中 |
CopyTickVolume | 将相应交易品种和时间范围的报价量历史数据复制到数组中 |
CopyRealVolume | 将相应交易品种和时间范围的交易量历史数据复制到数组中 |
CopySpread | 将相应交易品种和时间范围的价差历史数据复制到数组中 |
CopyTicks | 获取数组中 MqlTick 格式的分时报价 |
CopyTicksRange | 获取指定日期范围内的分时报价数组 |
iBarShift | 返回系列中包含指定时间的柱形的索引 |
iClose | 返回相应图表上柱形的收盘价(由‘shift’参数指示) |
iHigh | 返回相应图表上柱形的最高价(由‘shift’参数指示) |
iHighest | 返回相应图表上找到的最高价的索引(相对于当前柱形的偏移) |
iLow | 返回相应图表上柱形的最低价(由“shift”参数指示) |
iLowest | 返回相应图表上找到的最低价的索引(相对于当前柱形的偏移) |
iOpen | 返回相应图表上柱形的开盘价(由‘shift’参数指示) |
iTime | 返回相应图表上柱形的开盘时间(由‘shift’参数指示) |
iTickVolume | 返回相应图表上柱形的报价量(由‘shift’参数指示) |
iRealVolume | 返回相应图表上柱形的实际交易量(由‘shift’参数指示) |
iSpread | 返回相应图表上‘shift’参数指定的柱形的价差值 |
创建函数(详细)
MQL5 程序中的任何函数都是使用相同的模板创建的,我们在本系列的第一篇文章中已经对此进行了简要讨论:
ResultType Function_Name(TypeOfParameter1 nameOfParameter1, TypeOfParameter2 nameOfParameter2 …) { // Description of the result variable and other local variables ResultType result; // … //--- // Main actions are performed here //--- return resut; }
例 13.函数描述模板。
ResultType(和 TypeOfParameter)代表任何允许的数据类型。这可以是 int、double、类或枚举名称,或者您知道的任何其他内容。
函数运算也可以没有参数,或者没有显式的结果。然后,在结果类型或括号内的参数列表中插入的是单词“void”,它代表一个空的结果。
显然,如果 ResultType 为 void,则不需要返回数据,相应地,大括号内的最后一行(返回结果)就不需要指定,也不需要描述结果变量。
以下是一些简单的规则:
- 函数名称和参数名称必须符合标识符约定。
- return 运算符仅返回一个值,它无法返回更多。但也有解决方法,我们稍后会讨论。
- 一个函数不能在另一个函数内部描述,只能在所有函数外部描述。
- 您可以描述具有相同名称但具有不同数量(或不同类型)的参数和/或不同类型的返回值的多个函数。只要确保您能准确理解在特定情况下应该使用哪个函数即可。如果你能向不熟悉你的代码的人区分和解释这些差异,那么编译器也可以。
以下是一些示例,说明如何描述函数:
//+------------------------------------------------------------------+ //| Example 1 | //| Comments are often used to describe what a function does, | //| what date it needs and why. For example, like this: | | //| | //| | //| The function returns difference between two integers. | //| | //| Parameters: | //| int a is a minuend | //| int b is a subtrahend | //| Return value: | //| difference between a and b | //+------------------------------------------------------------------+ int diff(int a, int b) { // The action is very simple, we do not create a variable for the result. return (a-b); } //+------------------------------------------------------------------+ //| Example 1a | //| The function returns the difference between two real numbers. | //| | //| Function name is as in the previous example, but parameter type | //| differs | //| | //| Parameters: | //| double a is a minuendе | //| double b is a subtrahend | //| Return value: | //| difference between a and b | //+------------------------------------------------------------------+ double diff(double a, double b) { return (a-b); } //+------------------------------------------------------------------+ //| Example 2 | //| Illustrates the use of "void". | //| Calls (uses) the diff function | //+------------------------------------------------------------------+ void test() { // You can do whatever you want. // For example, use the function from Example 1. Print(diff(3,4)); // the result is -1 // Since when calling the diff function, integer parameters were // passed in parentheses, the result is also int. // Now let's try to call the same function with double precision parameters Print(diff(3.0,4.0)); // the result is -1.0 // Since the function is declared as "void", the "return" statement is not needed } //+------------------------------------------------------------------+ //| Example 3 | //| The function has no parameters to process. We could use | //| empty parentheses as in the previous example or explicitly use | //| the word "void" | //| Return value: | //| string nameForReturn is some name , always the same | | //+------------------------------------------------------------------+ string name(void) { string nameForReturn="Needed Name"; return nameForReturn; }
例 14.按模板列出的函数描述示例。
重要的是要理解,使用函数包括两个阶段:描述和使用。当我们描述一个函数时,这个函数还没有做任何事情。这仅仅是一个形式化的行为算法。例如,示例 14 中的 diff 函数可以用以下文字描述:
- 取两个整数(任意,事先未知)。
- 在算法中,将其中一个表示为 a ,另一个表示为 b 。
- 从 a 中减去 b 。
- 将计算结果(任何、事先未知)提供给调用者(返回到调用点)。
当我们谈到“任何、事先未知的”值时,这个表达式可以用“形式参数”代替。创建函数是为了对任何数据正式执行某些操作。
描述函数时使用的参数被称为“形式化的” 。
在示例 14 中,diff 函数包含两个形式参数,其他函数没有形式参数。一般来说,一个函数可以有很多个形式参数(最多63个)。
但为了得到特定的结果,必须调用(即使用)该函数。参见示例 14 中的函数测试,其中调用了 Print 和 diff 函数。这里,函数使用在调用时实际上非常具体的值:变量或常量的内容、文字(如我的例子)、其他函数的结果。
我们在调用时传递给函数的参数被称为“实际参数” 。
要调用任何函数,您需要指定其名称并在括号中列出实际参数。实际参数在类型和数量上应该与形式参数相对应。
在示例 14 中,“test”函数使用两个整数或两个双精度数来调用“diff”函数。如果我犯了一个错误并尝试写入一个或三个参数,我就会收到编译错误。
变量作用域
声明变量时,需要考虑它们的具体声明位置。
-
如果变量是在函数内部声明的(包括该函数的形式参数),其他函数将看不到该变量(因此将无法使用它) 。通常,这样的变量在函数被调用时“诞生”,并在函数完成其工作时“消亡”。这样的变量被称为局部变量。
一般来说,我们可以说变量的作用域是由代码中的整个“实体”决定的。例如,如果一个变量在花括号内声明,它只会在形成块的括号内可见,而不会在块外可见。函数的形式参数属于函数的“实体”,因此它们仅在该函数内可见,等等。这种变量的生命周期等于它所属的“实体”的生命周期。例如,在函数内部声明的变量在调用该函数时创建,在函数终止时销毁。
void OnStart() { //--- Local variable inside a function is visible to all blocks of that function, but not beyond it int myString = "This is local string"; // Curly braces describe a block inside a function { int k=4; // Block local variable - visible only inside curly braces Print(k); // It's ok Print (myString); } // Print(k); // Compilation error. The variable k does not exist outside the curly brace block. }
例 15.花括号块内的局部变量
- 如果在任何函数描述之外声明变量,则可以被我们应用程序的所有函数使用。在这种情况下,这种变量的生命周期等于程序的生命周期。这样的变量被称为全局变量。
int globalVariable = 345; void OnStart() { //--- Print (globalVariable); // It's ok }
例 16.全局变量对于我们程序中的所有函数都是可见的。
- 局部变量可以与全局变量同名,但在这种情况下,局部变量隐藏了给定函数的全局变量。
int globalVariable=5; void OnStart() { int globalVariable=10; // The variable is described according to all the rules, including the type. // If the type were not declared, this expression would change the global variable //--- Print(globalVariable); // The result is 10 - that is, the value of the local variable Print(::globalVariable); // The result is 5. To print the value of a global variable, not the local one, // we use two colons before the name }
例 17.局部变量和全局变量的相同名称。局部变量隐藏了全局变量。
静态变量
局部变量的描述有一种特殊情况。
如前所述,局部变量在函数完成后将失去其值。这通常正是预期的行为。然而,在某些情况下,即使在函数完成执行后,您也需要保存局部变量的值。
例如,有时需要保留对函数调用的计数器。另一项对交易者来说更常见的任务是组织一个函数来检查新烛形的开始。这需要在每次分时报价时获取当前时间值并将其与之前已知的值进行比较。当然,您可以为每个计数器创建一个全局变量。但是每个全局变量都会增加错误的概率,因为这些计数器由一个函数使用,而其他函数不应该改变甚至看到它们。
在这种情况下,当局部变量应该与全局变量有一样长的生命周期时,就会使用静态变量。它们的描述和普通的一样,只是在描述前面添加了 “static” 一词。以下示例中的 HowManyCalls 函数显示了此用法:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- HowManyCalls(); HowManyCalls(); HowManyCalls(); } //+------------------------------------------------------------------+ //| The function counts the number of requests | //+------------------------------------------------------------------+ void HowManyCalls() { //--- Variable description. The variable is local, but its lifetime is long. static int counter=0; // Since 'static' keyword is used, the variable is initialized only // before he first function call // (more precisely, before the OnInit function call) //--- Main actions counter++; // During program execution, the value will be stored till the end //--- Operation result Print( IntegerToString(counter)+" calls"); } // Script output: // 1 calls // 2 calls // 3 calls
例 18.使用静态变量。
该示例包含两个函数:HowManyCalls,它使用一个静态变量来计算对它的调用次数,并将结果打印到日志中;OnStart,它连续三次调用 HowManyCall。
按值和引用传递函数参数
默认情况下,函数仅使用数据副本,这些数据副本作为参数传递给函数(程序员说这种情况下数据是“按值”传输的)。因此,即使在函数内部的变量参数中写入某些内容,源数据也不会发生任何变化。
如果我们想在函数内部修改源数据,则可改变的形式参数必须用特殊图标 & (与号)来指定。这种描述参数的方法称为通过引用传递。
为了说明函数如何修改外部数据,让我们创建一个包含以下代码的新脚本文件:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart(void) { //--- Declare and initialize two local variables int first = 3; int second = 77; //--- Print their values BEFORE all changes Print("Before swap: first = " + first + " second = " + second); //--- Use the Swap function, which takes data by reference Swap(first,second); //--- See what happened Print("After swap: first = " + first + " second = " + second); //--- //--- Apply the CheckLocal function to the received data //--- This function takes parameters by value CheckLocal(first,second); //--- Print the result again Print("After CheckLocal: first = " + first + " second = " + second); } //+------------------------------------------------------------------+ //| Swaps the values of two integer variables | //| Data is passed by reference, so the originals will be modified | //+------------------------------------------------------------------+ void Swap(int &a, int& b) // It can be done in any way, both positions are correct { int temp; //--- temp = a; a = b; b = temp; } //+------------------------------------------------------------------+ //| Takes parameters by value, that is why changes happen | //| only locally | //+------------------------------------------------------------------+ void CheckLocal(int a, int b) { a = 5; b = 10; } // Script output: // Before swap: first = 3 second = 77 // After swap: first = 77 second = 3 // After CheckLocal: first = 77 second = 3
例 19. 通过引用传递参数。
这段代码定义了三个简短的函数:OnStart、Swap 和 CheckLocal。
CheckLocal 按值获取数据,因此可以使用副本,而 Swap 通过引用获取两个参数,因此可以使用源数据。OnStart 函数声明两个局部变量,然后打印这些变量的值,调用 Swap 和 CheckLocal 函数,并在每次交互后将其局部变量的值打印到日志中,以显示交互结果。再次提醒您,Swap 函数已经修改了传递给它的数据,但 CheckLocal 却无法做到这一点。
值得注意的是,所有复杂类型的变量(例如枚举、结构、对象等,以及任何数组)必须始终通过引用传递。
当尝试通过值传递此类变量时,编译器将生成错误。
我将再次简要列出变量和函数交互的基本规则:
- MQL5 语言中的全局变量可以在任何函数中直接使用,包括修改其值。
- 局部变量只能在声明它们的块内访问。
- 如果形式参数描述“按值”传输数据,则函数无法更改原始数据,即使它在内部更改了参数变量的值。但“通过引用”传递的数据可能会在其原始位置发生变化。
- 如果全局变量和局部变量具有相同的名称,则局部变量优先(换句话说,局部变量覆盖全局变量)。
- 全局变量的生存期等于程序的生存期,局部变量等于描述它们的块的生存期。
形式函数参数的默认值
可以为形式参数分配默认值。
例如,如果我们创建一个日志函数,我们可能需要将该函数的消息与所有其他终端消息区分开来。最简单的方法是在原始消息的开头添加前缀,在结尾添加后缀。必须始终指定字符串本身,否则函数的含义将丢失。“附加内容”可以是标准的,也可以是修改过的。
下面给出了最简单的代码来说明这个想法:
//+------------------------------------------------------------------+ //| Add a prefix and suffix to a sting | //+------------------------------------------------------------------+ string MakeMessage( string mainString, string prefix="=== ", string suffix=" ===" ) { return (prefix + mainString + suffix); }
例 20.具有默认形式参数的函数描述
调用此函数时,可以省略一个或两个具有默认值的参数。如果没有明确指定这些参数,该函数将使用描述中指定的值。例如:
Print ( MakeMessage("My first string") ); // Default prefix and suffix Print ( MakeMessage("My second string", "~~ ") ); // Prefix changed, suffix remains unchanged Print ( MakeMessage("My third string", "~~ ", " ~~") ); // Both actual parameter have been changed // Script output: // === My first string === // ~~ My first string === // ~~ My first string ~~
例 21.具有默认值的实际参数可以省略
具有默认值的参数只能连续,并且必须在所有其他没有此类值的参数之后描述。
如何使函数返回多个结果
如前所述,return 运算符只能返回一个结果。此外,该函数不能返回数组。但是,如果你真的需要更多的返回值呢?例如,如果您需要使用一个函数来计算烛形关闭时间和价格或获取可用工具的列表,该怎么办?首先,请尝试自己找到一个解决方案,然后与下面写的内容进行比较。
要解决标题中所述的问题,您可以使用以下方法之一:
- 创建一个复杂数据类型(例如,结构)并返回该类型的变量。
- 使用通过引用传递参数。当然,这不会帮助您使用 “return” 语句返回这些数据,但它允许您写下任何值然后使用它们。
- 使用全局变量(不推荐)。此方法与前一个方法类似,但对代码来说可能更危险。最好尽量减少使用全局变量,只有在绝对不可能没有它们的情况下。但如果你真的需要,你可以试着这样做。
全局变量修饰符:input 和 extern
使用全局变量时也存在“特殊情况”,其中包括:
- 使用 “input” 修饰符描述程序输入参数
- 使用 “extern” 修饰符
用 MQL5 编写的程序的每个输入参数都被描述为一个全局变量(在所有函数之外),并由位于描述的开头的 input 关键字指定。
input string smart = "The smartest decision"; // The window will contain this description
例 22.输入参数描述
通常,属性窗口的左列显示变量名称。然而,如果描述该变量的同一行包含了注释,如示例 22 中所示,则将显示该注释而不是变量名称。
在用 MQL5 编写的程序中,标记为 input 的变量可以以只读方式访问,但您不能在其中写入任何内容。这些变量的值只能在描述(在代码中)或从程序属性对话框中设置。
如果您正在创建 EA 交易或指标,您通常可以使用策略测试器优化这些变量的值。然而,如果您想从优化中排除某些参数,则需要在输入的单词开头添加字母 s 或修饰符 'static':
input double price =1.0456; // optimize sinput int points =15; // NOT optimize static input int unoptimizedVariable =100; // NOT optimize
例 23.使用 “sinput” 修饰符将变量从测试器优化中排除
如果您希望用户从输入字段的列表中选择值,则需要为每个字段添加一个枚举。枚举元素的内联注释也有效,因此您可以用任何人类语言显示 “Point Close Price(平仓价格点数)”,而不是像 POINT_PRICE_CLOSE 这样的名称。不幸的是,没有简单的方法来选择字段名称(注释)的文本语言。对于您使用的每种语言,您都必须编译一个单独的文件,这就是为什么大多数有经验的程序员更喜欢使用通用(英语)语言。
参数可以直观地分组,使其更易于使用。为了指定组名,可以使用特殊描述:
input group "Group Name"
例 24.参数组标头
下面是一个完整的例子,说明了所有这些功能:
#property script_show_inputs // Enumeration. Allows you to create a list box. enum ENUM_DIRECTION { // All inline comments next to lines describing parameters, // will be displayed instead of the names in the parameters window DIRECTION_UP = 1, // Up DIRECTION_DN = -1, // Down DIRECTION_FL = 0 // Unknown }; input group "Will be optimized" input int onlyExampleName = 10; input ENUM_DIRECTION direction = DIRECTION_FL; // Possible directions list input group "Will not be optimized" sinput string something = "Something good"; static input double doNotOptimizedMagickVariable = 1.618; // Some magic //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- } //+------------------------------------------------------------------+
示例 25 .描述输入参数的不同选项
图 6.参数对话框。组(input group)以颜色突出显示。绿色箭头表示替换变量名的注释值。
图 6 显示了根据示例 25 中的代码生成的选项对话框。它在不同的计算机上可能看起来略有不同,但无论如何,组标题都会被突出显示(我在图片中用蓝色突出显示了它们)。您还可以看到,没有内联注释的参数将使用变量名。如果有注释,编译器会使用它们而不是变量名,如绿色箭头所示的单元格。将示例 25 中的代码与图片进行比较;我希望它能帮助你理解一切。
还有一件事,并非所有初学者都会注意到每个参数的数据类型左侧的图标。例如,图中名为“Possible directions list(可能的方向列表)”的参数的数据类型为枚举,其图标( ) 表明它是一个列表。该字段的数据只能从有限的枚举中选择。其余的图标也是不言自明的。
任何参数的名称都不应超过 63 个字符(这个长度很长,因为真实名称通常要短得多)。
字符串参数长度不能超过 254 个字符。此外,参数名称越长,内容越短,因为它们作为一个连续的行存储在内存中。
记住这一限制很重要,特别是如果您要指定对程序很重要的某个网页的地址。有时地址真的很长,如果是这种情况,请尝试以其他方式传递地址,例如,将其硬编码为全局变量,而不是参数。当然,还有更好的解决方案,例如使用文件或从几个片段中“粘合”一个地址,只需记住参数值的限制为 254 个字符。
第二种特殊情况是‘外部(extern)’变量。
当开发人员编写一个大型程序并将其分成多个文件时,有时在一个文件中描述全局变量,而程序需要从其他文件访问该变量。我们不想使用 #include 指令来包含文件。MetaEditor 分别感知每个文件,因此在这种情况下无法提供帮助。
最常见的是,使用输入参数时会出现这种情况(在上一小节中进行了描述)。
这是可以使用‘extern’关键字的地方。
extern bool testComplete;
例 26.外部变量描述
此类变量可能未在该文件中初始化,并且在编译时,如果编译器能够找到该变量的内存地址,则该变量的内存地址很可能会被替换为同名的“真实”全局变量。但是,函数可以自由访问这些“形式”的数据,包括修改它,并且 IDE 不会遇到任何自动替换的困难。
终端全局变量
前几节中描述的局部和全局变量只能由当前程序访问。所有其他程序都不能使用此数据。但是,在某些情况下,程序需要相互交换数据,或者即使在终端关闭后,也有必要确保保存变量的值。
数据交换案例的一个例子可以是一个非常简单的指标,在其中您需要输出开仓所需的存款货币的资金量。看上去一切都很简单。通过搜索帮助目录,我们发现 MQL5 有一个特殊的函数 OrderCalcMargin ,可以计算所需的金额。我们尝试应用它,但却……失望了。这是因为您不能在指标中使用交易函数。从编译器层面上看,这是物理上禁止的,OrderCalcMargin 是一个交易函数。
因此,我们必须找到解决办法。一种选择是编写一个脚本或服务来计算所需的金额,然后将这些金额写入终端变量。然后我们的指标将读取该数据,而不是计算它。这个技巧是可行的,因为与指标不同,脚本和服务可以进行交易(参见本系列第一篇文章中的表格)。
让我们看看如何实现这种数据交换。首先,让我们使用向导创建一个脚本文件。我们将此文件命名为 “CalculateMargin.mq5”。
有一整套用于访问终端变量的预定义函数,其名称以前缀 GlobalVariable 开头。
使用这些和 OrderCalcMargin 函数使指标可以使用所需的数据,我们将创建一个新脚本:
//+------------------------------------------------------------------+ //| CalculateMargin.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" #property script_show_inputs //--- Script input parameters input double requiredAmount = 1; // Number of lots //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Description of local variables string symbolName = Symbol(); // Name of the current symbol string terminalVariableName; // Terminal variable name double marginBuy, marginSell; // Margin values (buy and sell) double currentPrice = iClose(symbolName,PERIOD_CURRENT,0); // Current price to calculate margin bool okCalcBuy, okCalcSell; // Indication of success when calculating margin up or down //--- Main operations // Calculate Buy margin okCalcBuy = OrderCalcMargin( ORDER_TYPE_BUY, // Order type symbolName, // Symbol name requiredAmount, // Required volume in lots currentPrice, // Order open price marginBuy // Result (by reference) ); // Calculate Sell margin okCalcSell = OrderCalcMargin( ORDER_TYPE_SELL, // Sometimes different amounts are needed for opening up and down symbolName, requiredAmount, currentPrice, marginSell ); //--- Operation result // Create a terminal variable name for Buy details terminalVariableName = symbolName + "BuyAmount"; // Write the data. If the global terminal variable does not exist, it will be created. GlobalVariableSet ( terminalVariableName, // Where to write marginBuy // What to write ); // Now we create another name - for the Sell details terminalVariableName = symbolName + "SellAmount"; // Write data for Sell. If there was no variable with the name stored in terminalVariableName, // create one GlobalVariableSet(terminalVariableName,marginSell); } //+------------------------------------------------------------------+
例 31.用于计算买入或卖出 1 手所需存款货币资金并将该数据保存在终端全局变量中的脚本
这里我们使用标准的 GlobalVariableSet 函数将数据写入终端变量。我认为在给定的例子中这些函数的使用是显而易见的。附加说明:全局终端变量的名称长度不得超过 63 个字符。
如果您在任何图表上运行此脚本,您将不会立即看到任何明显的结果。但是,您可以使用 <F3> 键或从终端菜单中选择“工具 -> 全局变量”来查看发生的情况。
图 7.终端变量菜单。
选择此菜单项后,将出现一个窗口,其中包含所有终端变量的列表:
图 8.带有全局终端变量列表的窗口
在图 8 中您可以看到我仅在 EURUSD 对上运行了脚本,因此只有两个变量可见:买入和卖出的金额,在本例中是相同的。
现在让我们创建一个使用这些数据的指标,同时我们将看到标准函数如何使用上面讨论过的处理变量的原理。
我们将此文件命名为“GlobalVars.mq5”。该指标中的主要操作将在 OnInit 函数内部执行,该函数在程序启动后立即执行一次。我们还添加了 OnDeinit 函数,当我们从图表中删除指标时,该函数会删除注释。OnCalculate 函数对于每个指标都是必需的,并且在每个分时报价时执行,该函数也存在于此指标中,但未使用。
//+------------------------------------------------------------------+ //| GlobalVars.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" #property indicator_chart_window //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Description of local variables string symbolName = Symbol(); // Symbol name string terminalVariableName; // Name of global terminal value double buyMarginValue, sellMarginValue; // Buy and Sell value bool okCalcBuy; // Indication that everything is OK when calling one of the variants of the GlobalVariableGet function //--- Main operations // Create a terminal variable name for Buy details terminalVariableName = symbolName + "BuyAmount"; // Use the first method to get the value of a global variable. // To get the result, the parameter is passed by reference okCalcBuy = GlobalVariableGet(terminalVariableName, buyMarginValue); // Change the name of the terminal variable - for Sell details terminalVariableName = symbolName + "SellAmount"; // Second way to get the result: return value sellMarginValue = GlobalVariableGet(terminalVariableName); //--- Output the result as a comment on the chart Comment( "Buy margin is " + DoubleToString(buyMarginValue) // Buy margin value, the second parameter // of the DoubleToString function is omitted +"\n" // Line break +"Sell margin is " + DoubleToString(sellMarginValue,2) // Margin value for sale, indicated the number of // decimal places ); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| The function will be called when the program terminates. | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Очищаем комментарии Comment(""); } //+------------------------------------------------------------------+ //| Custom indicator iteration function (not used here) | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- //--- return value of prev_calculated for the next call return(rates_total); } //+------------------------------------------------------------------+
例 32.在指标中使用全局终端变量。
在这个演示指标中我们需要读取一次终端变量并完成操作。因此主要代码放在 OnInit 函数中。这个例子可能看起来很大而且吓人,但实际上它非常容易阅读,特别是因为其中大部分都是注释。让我再次用文字描述 OnInit 函数中发生的情况:
- 在这个函数的第一个块中,我们声明了后面将需要的所有变量。
- 然后我们为全局终端变量创建了一个名称。
- 我们将所需的全局终端变量的值读入相应的局部变量中。
- 然后我们生成第二个变量的名称并在下一行读取其值。
- 最后一个动作是在左上角以注释的形式向用户输出一条消息(见图 9)。
请注意, GlobalVariableGet 函数有两个调用选项:使用返回值或通过引用传递的参数,而 DoubleToString 函数有一个具有默认值的参数。如果您在编辑器中键入示例文本以检查代码功能,而不是通过剪贴板复制,MetaEditor 将提示您这些细微差别。
图 9.指标运行结果
因为我使用了不同的方式调用 DoubleToString 函数来生成输出,所以顶部和底部行的注释看起来略有不同。在为顶部行格式化消息时,我省略了 DoubleToString 函数的第二个参数。该参数必须指定小数点后的字符数,默认为 8 个。对于底部行,我明确指定了这个值并指示程序输出两个字符。
请注意,指标必须在应用脚本的图表上启动,并且只能在脚本之后启动 - 以确保终端的全局变量在运行时存在。否则,指标运行时将出现错误,注释将不会出现。
就是这样,GlobalVariableSet 函数用于写入终端变量,而 GlobalVariableGet 函数用于读取它们。这些函数是程序员最常用的,但其他函数也很有用,所以我建议至少阅读它们在语言文档中的列表(链接位于本节开头)。
结论
让我们再次回顾一下今天涵盖的主题列表。如果您对列表中的任何一点不清楚,请返回文章中的相应位置并再次阅读,因为这些材料是其余工作的基础(好吧,也许除了全局终端变量之外,您通常可以不使用它们 — 但理解它们通常不会造成困难)。因此,在本文中我们讨论了:
- 数组:
- 可以是静态和动态的
- 可以是一维的,也可以是多维的
- 静态数组可以使用文字(花括号中)进行初始化
- 要使用动态数组,您需要使用标准函数来更改大小并找出当前大小
- 与函数相关的变量可以是
- 局部的(除静态外,其他情况均为短暂存在);可以为其添加 static 修饰符
- 全局(长寿命);您可以向其添加 extern 和 input 修饰符
- 可以传递函数参数的方式
- 通过引用
- 通过值
- 全局终端变量。与简单的全局程序变量不同,它们可用于在不同的程序之间交换数据。有一组特殊的函数供他们使用。
如果您记住了所有这些,并且列表上的任何项目都没有让您感到困惑,那么您就不再被视为新手了:您已经拥有良好的工作基础。剩下的就是弄清楚如何使用基本运算符,以及了解在编写指标和 EA 交易时 MQL5 语言相对于其他语言有哪些特性 - 然后您就可以开始编写有用的程序了。然而,要达到“专业”水平,您还需要了解十几个主题,包括面向对象编程,但所有这些主题仍然会以某种方式依赖于已经半准备好的基础。
愿语言文档与您同在......
本系列先前的文章:
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15357


谢谢你,好心人。
非常乐意...