Appearance
函数
函数是一个带有语句的具名代码块。程序的几乎整个应用算法都包含在函数之中。在函数之外,仅执行一些辅助操作,例如创建和删除全局变量。
当我们调用某个函数时,函数内部的语句才会执行。一些主要的函数会在各种事件发生时由终端自动调用。它们也被称为 MQL 程序的入口点或事件处理程序。特别地,我们已经知道,当在图表上运行一个脚本时,终端会调用其主函数 OnStart
。在其他类型的程序中,还有其他由终端调用的函数,我们将在涵盖 MQL5 API 交易架构的第五和第六章中详细讨论这些函数。
在本章中,我们将学习如何定义和声明一个函数,如何描述并向其传递参数,以及如何从函数中返回其运算结果。
我们还将讨论函数重载,即能够提供多个同名函数的能力,以及这种能力有何用处。
最后,我们将了解一种新的类型:函数指针。
函数定义
函数定义由其返回的值类型、标识符、括号内的参数列表以及函数体(一个包含语句的代码块)组成。参数列表中的参数用逗号分隔。每个参数都有一个类型、一个名称,并且可以有一个可选的默认值。
plaintext
返回值类型 函数标识符 ( [参数类型 参数标识符 = 默认值],... )
{
[语句]
...
}
允许创建没有参数的函数:此时没有参数列表,在函数名后面放置空括号(不能省略)。你可以在括号之间可选地写入 void
关键字,以强调没有参数。例如:
c++
void OnStart(void)
{
}
返回类型、参数列表中参数的数量和类型的组合被称为函数原型或签名。不同的函数可以具有相同的原型。
在前面的章节中,我们已经看到了像 OnStart
和 Greeting
这样的函数定义。现在让我们尝试实现一个计算斐波那契数的测试函数。这些数通过以下公式计算:
plaintext
f[0] = 1
f[1] = 1
f[i] = f[i - 1] + f[i - 2], i > 1
前两个数是 1,所有后续的数都是前两个数的和。我们给出这个数列的开头:1, 1, 2, 3, 5, 8, 13, 21, 34, 55...
你可以使用以下函数(FuncFibo.mq5
)计算给定索引处的数。
c++
int Fibo(const int n)
{
int prev = 0;
int result = 1;
for(int i = 0; i < n; ++i)
{
int temp = result;
result = result + prev;
prev = temp;
}
return result;
}
它接受一个 int
类型的参数 n
,并返回一个 int
类型的结果。n
参数带有 const
修饰符,因为我们不打算在函数内部更改 n
(这种对变量“权限”限制的显式声明是值得提倡的,因为它有助于避免意外错误)。
局部变量 prev
和 result
将存储数列中最后两个数的当前值。在对 i
的循环中,我们计算它们的和,得到数列的下一个数。在此之前,将旧的 result
值写入变量 temp
,以便在求和后将其转移到 prev
。
在循环执行指定次数后,result
变量包含所需的数。我们使用 return
语句从函数中返回该数。
函数的输入参数也是一个局部变量,在函数调用期间它将被初始化为实际值。这个值从调用函数的语句“外部”传递进来。
参数名称必须是唯一的,并且不能与局部变量名称匹配。
函数体是一个代码块,它定义了局部变量的作用域和生命周期。它们的定义和操作原理在“声明/定义语句”和“初始化”章节中进行了讨论。
函数调用
当函数的名称在表达式中被提及时,就会调用该函数。在函数名之后,应该有一对括号,括号中指明与函数参数(如果在其定义中有参数列表)相对应的参数,参数之间用逗号分隔。
稍后,我们将了解函数指针类型,它允许创建指向具有特定特征函数的变量,然后不是通过函数名,而是通过这个变量来调用函数。
继续以 Fibo
函数为例,让我们从 OnStart
函数中调用它。为此,我们创建一个变量 f
来存储得到的数,并在其初始化表达式中指明函数名 Fibo
,并在括号中给出一个整数(例如 10)作为参数。
c++
void OnStart()
{
int f = Fibo(10);
Print(f); // 89
}
我们并非必须创建一个变量来接收函数返回的值。相反,你可以直接从表达式中调用函数,例如 “2*Fibo(10)
” 或 “Print(Fibo(10))
”。然后,函数的值将在调用处被代入到表达式中。在这里,引入辅助变量 f
是为了在单独的语句中实现函数的调用和值的返回。
调用过程包括以下步骤:
- 调用函数(
OnStart
)的语句序列的执行被暂停; - 参数的值传递到被调用函数(
Fibo
)的输入参数n
中; - 被调用函数的语句开始执行;
- 当被调用函数完全执行完毕后,它将结果返回(记住函数内部的
return
语句); - 结果被写入变量
f
中; - 之后,
OnStart
函数继续执行,即把这个数打印到日志中(Print
)。
对于每次函数调用,编译器都会生成辅助的二进制代码(程序员无需为此担心)。这段代码的思路是,在调用函数之前,它将程序中的当前位置压入堆栈,调用完成后,再从堆栈中取出该位置,并使用它返回到函数调用之后的语句。当一个函数调用另一个函数,这个被调用函数又调用了其他函数,再调用第三个函数,以此类推时,在整个被调用函数的层级结构中,调用转移的返回地址会被累积在堆栈上(这就是堆栈名称的由来)。随着对嵌套函数调用的处理,堆栈将以相反的顺序被清空。请注意,堆栈也会为每个函数的局部变量分配内存。
参数和实参
在调用函数时传递给函数的实参,是相应函数形参的初始值。实参的数量、顺序和类型必须与函数原型相匹配。然而,实参的计算顺序是未定义的(请参阅“基本概念”部分)。根据源代码的具体情况和优化方面的考虑,编译器可能会选择一种对它来说方便的选项。例如,对于包含两个实参的列表,编译器可能会先计算第二个实参,然后再计算第一个实参。唯一可以保证的是,在函数调用之前,两个实参都会被计算。
每个实参都以与变量初始化相同的方式映射到相应的形参上,必要时会进行隐式类型转换。在函数开始执行之前,保证函数的所有形参都具有指定的值。例如,根据传递的实参,对 Fibo
函数的调用可能会产生以下效果(在注释中描述):
c++
// 警告
double d = 5.5;
Fibo(d); // 由于类型转换可能会丢失数据
Fibo(5.5); // 常量值被截断
Fibo("10"); // 从 'string' 到 'number' 的隐式转换
// 错误
Fibo(); // 参数数量错误
Fibo(0, 10); // 参数数量错误
所有的警告都与编译器执行的隐式转换有关,因为值的类型与形参的类型不匹配。这些警告应该被视为潜在的错误并加以消除。当实参数量过少或过多时,就会出现“参数数量错误”。
从理论上讲,函数的形参不一定必须有名称,也就是说,仅类型就足以描述形参。这听起来有点奇怪,因为在函数内部,如果形参没有名称,我们就无法访问它。然而,当基于某些标准接口创建程序时,有时必须编写与给定原型相符的函数。在这种情况下,函数内部的某些形参可能是不必要的。然后,为了明确表明这一事实,程序员可以省略它们的名称。例如,MQL5 API 要求实现 OnDeinit
事件处理函数,其原型如下:
c++
void OnDeinit(const int reason);
如果在函数代码中我们不需要 reason
形参,我们可以在描述中省略它:
c++
void OnDeinit(const int);
终端事件处理函数通常由终端本身调用,但是如果我们需要从我们的代码中调用一个类似的函数(带有匿名形参),那么无论形参是否有名称,我们都需要传递所有的实参。
值参数和引用参数
参数可以通过两种方式传递给函数:按值传递和按引用传递。
到目前为止,我们所看到的所有情况都是按值传递。这种方式意味着调用代码片段准备的参数值会被复制到一个新的变量中,这个新变量就是函数对应的输入变量。除此之外,实参和输入变量没有其他关联。函数内部对该变量进行的所有后续操作,都不会对实参产生任何影响。
若要描述一个引用参数,需在类型的右侧添加一个与符号 &
。很多程序员喜欢把与符号附加到参数名上,以此强调该参数是对给定类型的引用。例如,以下几种写法是等价的:
c++
void func(int ¶meter);
void func(int & parameter);
void func(int& parameter);
当调用函数时,不会为引用参数创建对应的局部变量。相反,为该参数指定的实参在函数内部会以输入参数的名称(别名)形式可用。这样一来,值不会被复制,而是在内存的同一地址处被使用。所以,函数内对参数的修改会反映在与其关联的实参状态上。由此会引出一个重要特性。
你只能将变量(左值,参见赋值运算符)指定为引用参数的实参。否则,会出现 “参数按引用传递,需要变量” 的错误。
按引用传递在以下几种情况下会被使用:
- 通过避免值的复制来提高程序的效率。
- 当使用
return
语句返回单个值不足以满足需求时,将修改后的数据从函数传递给调用代码。
第一点对于像字符串或数组这样可能较大的变量尤为重要。
为了区分引用参数的第一个和第二个用途,建议函数的编写者在函数内部不期望参数发生改变时,添加 const
修饰符。这会提醒你,也能让其他开发者清楚,将变量传递到函数内部不会产生副作用。
如果可能的话,不对引用参数使用 const
修饰符可能会在整个函数调用层次结构中引发问题。事实上,调用这类函数时需要非常量实参。否则,会出现 “常量变量不能按引用传递” 的错误。结果可能会逐渐导致,为了让代码能够编译,所有函数中的所有参数都要去掉 const
修饰符。实际上,这会扩大变量意外损坏这类潜在错误的发生范围。应该采取相反的做法来纠正这种情况:在不需要返回和修改值的地方都加上 const
。
为了比较在 FuncDeclaration.mq5
脚本中传递参数的方式,实现了几个函数:FuncByValue
按值传递,FuncByReference
按引用传递,FuncByConstReference
按常量引用传递。
c++
void FuncByValue(int v)
{
++v;
// 对 v 进行其他操作
}
void FuncByReference(int &v)
{
++v;
}
void FuncByConstReference(const int &v)
{
// 错误
// ++v; // 'v' 是常量,不能修改
Print(v);
}
在 OnStart
函数中,我们调用所有这些函数,并观察它们对作为实参使用的 i
变量的影响。注意,按引用传递参数不会改变函数调用的语法。
c++
void OnStart()
{
int i = 0;
FuncByValue(i); // i 不会改变
Print(i); // 0
FuncByReference(i); // i 会改变
Print(i); // 1
FuncByConstReference(i); // i 不会改变,值为 1
const int j = 1;
// 错误
// 'j' 是常量变量,不能按引用传递
// FuncByReference(j);
FuncByValue(10); // 正常
// 错误: '10' 是参数按引用传递,需要变量
// FuncByReference(10);
}
字面量只能传递给 FuncByValue
函数,因为其他函数要求实参是引用,也就是变量。
不能使用变量 j
调用 FuncByReference
函数,因为 j
被声明为常量,而这个函数声明了有能力(或意图)改变其参数,因为它没有配备 const
修饰符。这会产生 “常量变量不能按引用传递” 的错误。
该脚本还描述了 Transpose
函数:它通过引用传递一个二维数组,对一个 2x2 矩阵进行转置。
c++
void Transpose(double &m[][2])
{
double temp = m[1][0];
m[1][0] = m[0][1];
m[0][1] = temp;
}
在 OnStart
函数中调用它,展示了局部数组 a
的内容按预期发生了改变。
c++
double a[2][2] = {{-1, 2}, {3, 0}};
Transpose(a);
ArrayPrint(a);
在 MQL5 中,数组参数总是作为动态数组的内部结构传递(参见 “数组的特性” 部分)。因此,对这样一个参数的描述,其第一维的大小必须是开放的,也就是第一对方括号内是空的。
如果需要,这并不妨碍将实际参数(一个具有固定大小的数组)传递给函数(就像我们的例子一样)。然而,像 `ArrayResize` 这样的函数将无法调整这种伪装的固定数组的大小或以其他方式对其进行重组。
参数和实参的数组除第一维之外的所有维度的大小必须匹配。否则,会出现 “不允许参数转换” 的错误。特别是,在示例中定义了 `TransposeVector` 函数:
```c++
void TransposeVector(double &v[])
{
}
在 OnStart
函数中,尝试在二维数组 a
上调用它的代码被注释掉了,因为这会产生上述错误:数组维度不匹配。
除了按值或按引用传递参数之外,还有另一种方式:传递指针。与 C++ 不同,MQL5 只支持对象类型(类)的指针。我们将在第三部分探讨这个特性。
可选参数
在描述函数时,MQL5 允许为参数指定默认值。为此,需使用初始化语法,即在参数右侧的 '=' 符号后放置相应类型的字面量。例如:
void function(int value = 0);
调用函数时,可省略这些参数的实参。此时,它们的值将被设为默认值。这类参数被称作可选参数。
可选参数必须位于参数列表末尾。换句话说,若第 i 个参数声明时带有初始化,那么后续所有参数也都必须有初始化。否则,会出现编译错误 “missing default value for parameter”(参数缺少默认值)。以下是存在此问题的函数描述:
double Largest(const double v1, const double v2 = -DBL_MAX,
const double v3);
有两种解决办法:要么让参数 v3
也有默认值,要么让参数 v2
变为必选参数。
调用函数时,只能从右至左省略可选参数。也就是说,若函数有两个参数且均为可选参数,调用时不能跳过第一个参数而指定第二个参数。传入的单个值会与第一个参数匹配,第二个参数则会被视为省略。若两个参数都省略,仍需使用空括号。
以寻找三个数中的最大值的函数为例。第一个参数是必选的,后两个是可选的,默认值为 double
类型的最小可能值。这样,在未显式传入值时,每个参数肯定小于(或在极端情况下等于)其他所有参数。
double Largest(const double v1, const double v2 = -DBL_MAX,
const double v3 = -DBL_MAX)
{
return v1 > v2 ? (v1 > v3 ? v1 : v3) : (v2 > v3 ? v2 : v3);
}
可以按如下方式调用该函数:
Print(Largest(1)); // 没问题: 1
Print(Largest(0, -2)); // 没问题: 0
Print(Largest(1, 2, 3)); // 没问题: 3
借助可选参数,MQL5 在自定义函数中实现了可变数量参数的函数概念。
MQL5 不支持像 C++ 那样使用省略号语法来定义带有可变数量参数的函数。不过,MQL5 API 中有一些内置函数是用省略号描述的,可接受任意数量的可变参数。例如 Print
函数,其原型如下:void Print(argument, ...)
。所以,我们可以用最多 64 个用逗号分隔的参数(数组除外)调用它,它会将这些参数显示在日志中。
返回值
函数可以返回内置类型的值、具有内置类型字段的结构体,以及指向函数的指针和指向类对象的指针。类型名称写在函数定义中函数名的前面。如果函数不返回任何内容,则应将其指定为 void
类型。
要从数组函数中返回值,必须使用通过引用传递的参数(请参阅“值参数和引用参数”)。
使用 return
语句返回一个值,在 return
关键字后面指定一个表达式。可以使用以下两种形式中的任何一种:
c
return expression ;
或者:
c
return ( expression ) ;
如果函数的类型为 void
,那么 return
语句会简化为:
c
return ;
在 void
函数中,return
语句不能包含任何表达式:编译器将生成错误“'return' - 'void' 函数返回了一个值”。
从理论上讲,对于这类函数,在函数体所在的代码块末尾并非必须使用 return
。我们在 OnStart
函数的示例中看到过这一点。
如果函数的类型不是 void
,那么 return
语句是必须的。如果没有 return
语句,将会出现编译错误“并非所有控制路径都返回一个值”。
c
int func(void)
{
if(IsStopped()) return; // 错误:函数必须返回一个值
// 错误:并非所有控制路径都返回一个值
}
需要注意的是,函数体中可以有多个 return
语句。特别是在根据条件提前退出的情况下。任何 return
语句都会在其所在的位置中断函数的执行。
如果一个函数必须返回一个值(因为它的类型不是 void
),并且在 return
操作符中没有指定返回值,编译器将生成错误“函数必须返回一个值”。下面是 func
函数的符合编译器要求的版本(FuncReturn.mq5
)。
c
int func(void)
{
if(IsStopped()) return 0;
return 1;
}
如果返回值与指定的函数类型不同,编译器将尝试进行隐式转换。如果这些类型需要显式转换,则会生成一个错误。
为了返回一个值,会隐式地创建一个临时变量,并将其提供给调用代码使用。
在我们学习了对象类型(请参阅“类”这一章节)以及从函数中返回指向对象的指针的能力之后,我们将回过头来探讨如何安全地传递它们。与 C++ 不同,MQL5 中的函数不能返回引用。尝试在结果类型中使用 &
来声明函数会导致出现“'&' - 不能使用引用”的错误。
函数声明
函数声明描述了函数的原型,但不指定函数体。函数体的位置用分号替代。
声明对于编译器而言是必要的,这样它就能在后续代码片段中检查函数调用是否正确,包括函数名的使用、参数的传递以及结果的获取。
完整的函数定义(包含函数体)同样也是一种声明,所以在定义函数之后无需额外进行声明。
例如,上面提到的 Fibo
函数的声明可以写成如下形式:
c
int Fibo(const int n);
当程序由多个源文件构建时,会分别使用函数声明和定义:声明会放在扩展名为 .mqh
的头文件中(请参阅关于 #include
预处理指令的部分),该头文件会被包含在使用此函数的文件里,而函数定义则只在其中一个文件里实现。声明和定义中的函数签名保持一致可避免出错。换句话说,单一的声明能确保对整个源代码所做的修改具有一致性。
若声明了一个函数并在代码中调用它,但没有给出与之完全匹配的定义,编译器会抛出错误:“函数 'Name' 必须有函数体”。这种情况通常是由于声明或定义中存在拼写错误或不准确之处,或者在修改源代码的过程中,部分修改已完成,而另一部分可能被遗忘了。
如果声明了函数但在任何地方都未使用,编译器也不会要求对其进行定义——这样的元素会直接从二进制程序中“剔除”。
在“声明/定义语句”部分,我们探讨了 Init
函数(脚本 StmtDeclaration.mq5
)的示例,该函数用于初始化变量。其中特别展示了一个问题,即全局变量 k
不能在 Init
函数之前定义,因为 k
的初始值是通过调用 Init
函数获得的。编译器会报错“'Init' 是未知标识符”。
现在我们知道,这样的问题可以通过声明来解决。在 FuncDeclaration.mq5
脚本中,我们在变量 k
之前添加了 Init
函数的前置声明,而将 Init
函数的定义放在 k
之后:
c
// 前置声明
int Init(const int v);
// 在添加上述前置声明之前
// 这里会报错:'Init' 是未知标识符
int k = Init(-1);
int Init(const int v)
{
Print("Init: ", v);
return v;
}
现在,该脚本可以正常编译。从技术层面讲,在这种情况下,我们也可以不进行前置声明,直接将函数定义移到变量之前。我们这样做是为了解释这个概念。然而,在语言元素相互依赖的情况下(例如类),在同一个文件中不进行前置声明是无法实现的。
递归
允许在一个函数内部的语句中调用该函数自身。这样的调用被称为递归调用。
让我们回到计算斐波那契数列的例子。根据计算每个斐波那契数的公式(即除了前两个数等于 1 之外,每个数都是前两个数的和),很容易编写一个用于计算斐波那契数的递归函数。
c
int Fibo(const int n)
{
if(n <= 1) return 1;
return Fibo(n - 1) + Fibo(n - 2);
}
一个递归函数必须能够在不进行递归的情况下返回控制权,就像我们这个例子中在 if
条件语句里针对索引 0 和 1 的情况那样。否则,函数调用序列可能会无限持续下去。在实际情况中,由于未完成的函数调用会累积在一块被称为栈的有限内存区域中(请参阅“声明/定义语句”部分,以及“描述数组”部分中的“堆”和“栈”侧边栏),函数迟早会因“栈溢出”运行时错误而终止。这个问题在 FiboEndless
函数中有所体现。
c
int FiboEndless(const int n)
{
return FiboEndless(n - 1) + FiboEndless(n - 2);
}
请注意,这不是一个编译错误。在这种情况下,编译器甚至不会生成警告(尽管从技术上讲它是可以做到的)。错误会在脚本执行期间出现,并会被打印到终端的“专家”日志中。
递归不仅可以在函数调用自身时发生。例如,如果函数 F
调用函数 G
,而函数 G
又反过来调用函数 F
,这种情况就是间接递归。因此,递归可以由任何深度的循环调用所引发。
函数指针(typedef)
MQL5 具备 typedef
关键字,此关键字可用于描述一种特殊的函数指针类型。
与 C++ 不同,在 C++ 里 typedef
应用范围更广,而在 MQL5 中 typedef
仅用于函数指针。
新类型声明的语法如下:
plaintext
typedef function_result_type ( *function_type )( [list_of_input_parameters] ) ;
function_type
标识符定义了一个类型名,它成为指向任何返回 function_result_type
类型值且接受输入参数列表(list_of_input_parameters
)的函数的指针的同义词(别名)。
例如,我们可以有两个具有相同原型(两个 double
类型的输入参数,结果类型也是 double
)的函数,它们执行不同的算术运算:加法和减法(FuncTypedef.mq5
)。
cpp
double plus(double v1, double v2)
{
return v1 + v2;
}
double minus(double v1, double v2)
{
return v1 - v2;
}
它们的通用原型可以轻松描述,以便用作指针:
cpp
typedef double (*Calc)(double, double);
这行代码在程序中引入了 Calc
类型,借助该类型,你可以定义一个变量或参数,用于存储或传递指向任何具有此原型的函数的引用,包括 plus
和 minus
这两个函数。此类型是一个指针,因为在描述中使用了字符 *
(*Calc
)。在学习面向对象编程时,我们将深入了解星号在指针方面的特性。
使用这类指针来创建自定义算法十分便捷,这些算法能够根据输入数据“即时”调用与别名对应的不同函数。
具体而言,我们可以引入一个通用的计算器函数:
cpp
double calculator(Calc ptr, double v1, double v2)
{
if(ptr == NULL) return 0;
return ptr(v1, v2);
}
它的第一个参数被声明为 Calc
类型。正因如此,我们可以向其传递任意具有合适原型的函数,进而执行某种操作,而计算器函数本身并不知晓该操作的具体内容。它通过委托调用指针来实现这一点:ptr(v1, v2)
。由于 ptr
是一个函数指针,这种语法不仅类似于函数调用,实际上也确实调用了指针所指向的函数。
需要注意的是,我们预先将 ptr
参数与特殊值 NULL
进行比较(NULL
对于指针而言等同于零)。这是因为指针可能不指向任何地方,也就是可能未被初始化。所以,在脚本中,我们定义了一个全局变量:
cpp
Calc calc;
它不指向任何函数。若没有对 NULL
的“保护”,使用“空”指针 calc
调用 calculator
函数会导致运行时错误“无效的函数指针调用”。
在第一个参数中使用不同的指针调用 calculator
函数会得到以下结果(注释中显示):
cpp
void OnStart()
{
Print(calculator(plus, 1, 2)); // 3
Print(calculator(minus, 1, 2)); // -1
Print(calculator(calc, 1, 2)); // 0
}
需要注意的是,如果没有显式初始化,所有函数指针都会被填充为零值。这适用于该类型的全局变量和局部变量。
使用 typedef
定义的指针类型可以从函数中返回,例如:
cpp
Calc generator(ushort type)
{
switch(type)
{
case '+': return plus;
case '-': return minus;
}
return NULL;
}
此外,函数指针类型常用于回调函数(callback,参见 FuncCallback.mq5
)。假设我们有一个 DoMath
函数,它执行冗长的计算(可能在一个单独的库中实现)。从用户界面的便利性和友好性角度来看,向用户显示进度指示会很棒。为此,你可以定义一种特殊的函数指针类型,用于通知工作完成的百分比(ProgressCallback
),并在 DoMath
函数中添加该类型的参数。在 DoMath
代码中,应该定期调用传递进来的函数:
cpp
typedef void (*ProgressCallback)(const float percent);
void DoMath(double &bigdata[], ProgressCallback callback)
{
const int N = 1000000;
for(int i = 0; i < N; ++i)
{
if(i % 10000 == 0 && callback != NULL)
{
callback(i * 100.0f / N);
}
// 长时间的计算
}
}
然后,调用代码可以定义所需的回调函数,将其指针传递给 DoMath
函数,并在计算进行过程中接收更新。
cpp
void MyCallback(const float percent)
{
Print(percent);
}
void OnStart()
{
double data[] = {0};
DoMath(data, MyCallback);
}
函数指针仅适用于 MQL5 中定义的自定义函数。它们不能指向 MQL5 API 的内置函数。
内联
为了提高代码效率,现代编译器常常会采用以下技巧。在生成可执行代码时,一些函数调用会直接被函数体(其语句)所替代。这种技术被称为内联。通过避免与函数调用和返回的组织相关的开销,它加快了操作速度。从程序员的角度来看,内联不会带来任何变化。
MQL5 默认支持内联。如果有必要,可以禁用内联,但仅在代码性能分析模式下才行。在 MQL5 中,inline
关键字是为了与 C++ 源代码兼容而保留的。在函数定义前使用或不使用该关键字,都不会影响生成的程序。