English Русский Español Deutsch 日本語 Português
preview
从基础到中级:变量 (III):

从基础到中级:变量 (III):

MetaTrader 5示例 | 13 三月 2025, 08:44
294 0
CODE X
CODE X

概述

此处提供的材料仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用该应用程序。

在上一篇文章“从基础到中级:变量(II)”中 ,我们讨论并解释了如何在我们的代码中使用静态变量。这些变量使我们能够避免不必要地使用全局变量。所以,我们对变量主要部分的介绍已经完成了。然而,仍然存在每个变量可以包含的数据类型的问题,这仍然与这个问题有一定的相关性。我不会在变量主题的框架内考虑这一方面。我们将在另一个主题中讨论这个问题。

然而,如果我们已经讨论了局部和全局变量,如何以及为什么将变量声明为常量,甚至如何使用静态变量,那么在这个话题上还有什么好说的呢?虽然很多人不这么认为,但有一种特殊类型的变量通常可以被视为常数,但它仍然是一种特殊的变量。我说的是函数。函数是一种特殊类型的变量,尽管原则上许多程序员并不这么认为。

让我们开始一个新的主题,在这个主题中,我们将尝试理解函数是一个特殊的变量。


特殊变量:函数

当我们谈论函数时,对于具有基本编程知识的人来说,首先想到的是使用额外的调用来执行我们应用程序中的某些过程。然而,这个定义并不完全充分,这是因为有些子程序不是函数,而是过程。这两种类型之间存在细微的概念差异。主要区别在于,函数返回一个值,或者更确切地说,返回一种数据类型。与过程不同,过程执行后不会返回任何数据。但是,这两种类型都可以通过其参数修改和返回不同的值。

现在不用担心,我只是简要概述了稍后将更详细讨论的内容。但这个小小的区别是必要的,这样你才能理解我为什么说函数是一种特殊类型的变量。

在这里对不同的语言进行比较是很重要的。这是因为,根据我们未来想要学习的语言,这里讨论的概念可能不适用。这是因为每种语言都以自己的方式执行任务。

在 JavaScript 和 Python 等脚本语言中,函数通常以常量变量的形式实现。然而,在 C 和 C++ 中,函数既可以充当常量变量,也可以充当能够修改函数内静态变量值的变量,而不需要传递参数。这对一些人来说可能听起来不寻常,甚至违反直觉,因为这似乎是不可能的。然而,在 C 和 C++ 中,指针允许实现此功能。通过使用指针引用函数内的静态变量,调用该函数的外部代码可以修改该静态变量的值。

这一特性使得 C 和 C++ 编程既具有挑战性又功能强大。虽然它引入了潜在的风险,但它也为开发人员提供了高度的灵活性。在纯 MQL5 中,我们发现自己介于 JavaScript/Python 的函数处理和 C 与 C++ 的功能之间。虽然我们没有像 C 和 C++ 那样的灵活性,但 MQL5 提供了更安全的编码环境。

为了超越可能相当枯燥的理论,让我们探索一些简单的例子,说明如何将函数用作常量变量。一种常见的情况是使用函数作为定义值的手段 — 除了我们使用更有意义和代表性的名称而不是直接赋值。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(One_Radian());
07. }
08. //+------------------------------------------------------------------+
09. double One_Radian()
10. {
11.     return 180. / M_PI;
12. }
13. //+------------------------------------------------------------------+

代码 01

在代码示例 01 中,我们有一个非常简单的例子。当执行此代码时,您将看到如下所示的结果。


图 01

此时,您可能会问自己:为什么要这么做?将第 11 行的计算直接放在第 06 行并打印结果不是更容易吗?是的,确实如此。然而,如果这是你的想法,这意味着你还没有完全理解我想用这个例子表达的观点。这里的目的不是执行计算本身,目标是生成一个可以在整个代码中全局使用的常量。如果每次需要一个常量的值时,你都必须重新输入代码来创建它,那么改进甚至修正代码时的工作量将是巨大的。然而,如果所有内容都包含在一个具有有意义名称的函数中,该函数生成常数,那么这个过程将更简单、更快、更流畅,使你作为程序员更有效率。

考虑开发一个 3D 程序的情况,你需要将度数转换为弧度 — 不仅仅是在一行代码中,而是在分散在整个代码库中的数百行代码中。我来问你一下:每次键入计算结果,或者将所有内容放入代码示例 01 中所示的函数中,哪种更容易?

现在,这可能是一个应用程序,但如果你发现它微不足道或毫无意义,我们用从前面的文章中获得的知识创建一些更复杂的东西怎么样?这可以按如下所示完成:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Counting();
07.     Print(Counting());
08.     Counting();
09.     Counting();
10.     Print(Counting());
11. }
12. //+------------------------------------------------------------------+
13. ulong Counting(void)
14. {
15.     static ulong Tic_Tac = 0;
16. 
17.     Tic_Tac = Tic_Tac + 1;
18. 
19.     return Tic_Tac;
20. }
21. //+------------------------------------------------------------------+

代码 02

当执行代码示例 02 时,您将得到与下图所示类似的结果:


图 02

换句话说,如果第一个例子对你来说不太合适,也许第二个例子可能会。这是因为我们可能需要计算一个事件在给定应用程序中发生的次数。许多人会使用全局变量来跟踪此类事件。然而,正如前几篇文章所讨论的,使用全局变量有一个缺点。通过使用具有类似目的的函数,我们消除了全局变量的不便,同时确保了代码的稳定性和安全性。同时,它允许我们以更简单、更快、更实用的方式修改逻辑。


预定义变量

除了我们已经涵盖的关于变量和常量的主题外,MQL5 中还有其他重要类型需要提及。这是因为它们是广泛使用语言功能的程序不可或缺的一部分。

在这里,我们将简要介绍这些特殊类型的变量。了解这些变量的存在并将其用于特定任务是至关重要的。

在文档中,您可以在预定义变量下找到有关这些变量的更多详细信息。不过,我们在这里不会关注其中任何一个特定的预定义变量。重要的是要理解,尽管它们被称为预定义变量,但它们不是典型的变量。在我们的代码中,它们被视为常量。然而,对于 MetaTrader 5 来说,它们确实是变量。尽管在我们的代码中被视为常量,但在某些情况下,我们可以使用 MQL5 中的某些函数或过程来修改这些变量的值。

亲爱的读者,理解这一点非常重要。如果你试图直接修改其中一个变量的值,你会遇到错误,你的代码将无法编译。我所说的“直接修改”是指尝试做类似下面示例的事情:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(_LastError);
07.     _LastError = 2;
08.     Print(_LastError);
09. }
10. //+------------------------------------------------------------------+

代码 03

如果您是 MQL5 的新手,请密切关注我将要解释的内容。在代码示例 03 的第 6 行中,我们指示终端打印存储在预定义变量 _LastError 中的当前值。由于这是 MQL5 中的预定义变量,因此无需声明它。编译器可以自动识别它。

但是,当编译器遇到代码示例 03 中的第 7 行时,将触发错误,如下图所示:

图 03

为什么会发生这种情况?原因是 MQL5 中的这些预定义变量在编程级别被视为常量。然而,在 MetaTrader 5 的代码执行级别,这些相同的变量不被视为常量。

起初,这可能会让人感到困惑和难以理解,尤其是对于那些刚刚开始编程的人来说。然而,亲爱的读者,在深入了解预定义变量的更多细节之前,您必须了解以下内容:由于这些变量将存在于任何 MQL5 代码中,因此您不应尝试创建与这些预定义变量同名的另一个变量。这样做将违反平台规定的安全协议。

也就是说,尽管如代码示例 03 所示,尝试直接访问会导致错误,但 MQL5 语言本身提供了出于一般目的修改其中一些预定义变量的方法。没有具体的规则规定应该为这些变量分配什么值。然而,在可能的情况下,任何修改都应该有正当的理由。你不应该仅仅因为技术上可行就修改这些变量的值,除非你正在学习语言本身,就像我们在这里做的那样。

例如,通过使用 SetUserError 函数,我们可以为预定义变量 _LastError 设置一个值,但并非任何值。我们可以分配的值是有限的,因为有一个错误代码列表保留供 MQL5 本身使用。对于许多人来说,这似乎令人失望,因为他们可能认为这是一种限制。但事实并非如此。

要在实践中看到这一点,您可以使用下面显示的代码:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(_LastError);
07.     SetUserError(2);
08.     Print(_LastError);
09. }
10. //+------------------------------------------------------------------+

代码 04

该代码确实可以编译并执行。但是,当您在 MetaTrader 5 中运行它时,您将看到类似下图所示的内容。


图 04

那个奇怪的数字是什么?请等一等,在代码示例 04 的第 7 行中,我们分配与代码示例 03 的第 7 行相同的值。好吧,由于我们讨论的原因,代码示例 03 没有编译成功,但图 04 中的这个值并不是我所期望的。

事实上,亲爱的读者,显示的值是您打算分配给 _LastError 变量的值,但它已经被偏移了。这样做是为了防止它与任何预定义的错误值冲突。要查看您想要分配给 _LastError 的确切值,需要进行一些小的调整。但在我们研究这一调整之前,让我们先了解一些其他的事情。通常,许多程序员不喜欢在代码中直接使用预定义变量的名称。通常更建议使用返回预定义变量值的函数。虽然没有什么能阻止您使用如上所示的代码,但更常见(也是通常首选)的方法是使用如下所示的编码结构:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(GetLastError());
07.     SetUserError(2);
08.     Print(GetLastError());
09. }
10. //+------------------------------------------------------------------+

代码 05

请注意,该代码基本上做了同样的事情。然而,对于许多人来说,代码 05 比代码 04 更易读。通过这种方式,我们让其他阅读代码的程序员知道我们正在访问预定义变量的值。您是否注意到,上一主题中讨论的内容也适用于这里?访问预定义变量以进行后续读取的方式可以是任何方式,但改变其值的方法将始终相同。例如,要从 _LastError 变量中删除任何值,我们不能使用上述代码第 7 行所示的过程。如果我们这样做,我们将分配一个新的错误值。清除或删除 _LastError 错误的正确方法是使用 MQL5 中提供的另一个过程。如下所示:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(GetLastError());
07.     SetUserError(2);
08.     Print(GetLastError());
09.     SetUserError(0);
10.     Print(GetLastError());
11.     ResetLastError();
12.     Print(GetLastError());
13. }
14. //+------------------------------------------------------------------+

代码 06

亲爱的读者,如果这是你第一次遇到这种情况,请密切关注。执行此代码时,结果如下所示。


图 05

请注意以下几点:当执行第 6 行时,将会打印出图 05 中看到的第一行,表明 _LastError 变量中没有错误或值。执行第 7 行之后,我们就会为 _LastError 变量分配一个值,如前所述。然而,许多初学者尝试使用第 9 行来清除 _LastError 变量。但是,当执行此操作并打印结果时,我们得到的值不是零。为什么呢?原因与我之前提到的相同。当我们使用 SetUserError 在 _LastError 中设置一个值时,该值是偏移量。这就是为什么试图使用此函数设置任何值都不会产生预期结果的原因。但是,由于我们的目标是在 _LastError 变量中设置零值,因此正确的方法是使用第 11 行。当执行第11行中的过程时,值零确实将被分配给 _LastError。

对于所有其他预定义变量也应注意这一点。当然,我不会逐一介绍它们,因为您可以参考文档来了解为这些变量设置值的正确过程(如果有的话)。但是,可以直接读取这些值,如主题开头所示,即使用预定义变量本身的名称。

但请等一等。我遵循并理解了这里的解释,但我仍然没有完全掌握你在整个主题中多次提到的价值抵消的概念。你能进一步解释一下吗?这样,我就可以使用我分配给 _LastError 变量的值,而不是在终端中打印变量值时看到那些奇怪的数字。

事实上,这是一个合理的要求,值得彻底解释。让我们在一个新的主题中讨论它,因为我们还将涉及其他相关概念。


枚举和常量定义

在我看来,在任何编程语言中定义最具挑战性的方面之一是语言中定义和枚举的存在。亲爱的读者,别误会我的意思。两者都被广泛使用,极大地帮助了编程。然而,仅仅通过查看文档很难确定什么是枚举,什么是定义。这是因为除了说明所使用的建模类型的文档外,没有其他方法可以知道这一点。

暂且不深入探讨这些概念的细节(因为我们稍后会介绍它们),我们可以从理解每个值代表什么开始。更重要的是,我们应该专注于理解代码中常见的一些元素,比如乍一看可能没有多大意义的文本。下面的代码中可以看到一个例子:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. int OnInit()
05. {
06.    return INIT_SUCCEEDED;
07. };
08. //+------------------------------------------------------------------+
09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
10. {
11.    return rates_total;
12. };
13. //+------------------------------------------------------------------+

代码 07

在第 6 行中,我们有一些对许多人来说可能没有多大意义的东西,尽管它几乎出现在每个指标代码中。亲爱的读者,我们稍后会更详细地讨论这个问题。但我向您展示这段代码(代码 07)只是为了举例说明您在许多代码中经常遇到的问题。尽管保留字 return 需要一个变量或值,但这里我们使用的是字符串。但是这个字符串是什么意思,为什么我们使用它而不是另一个?事实上,我们可以使用其它东西。但让我们一步一个脚印吧。

如果您查看常量、枚举和结构,您将看到 MQL5 中定义了许多常量和枚举。这些枚举和常量,就像您在代码 07 第 6 行看到的一样,在编程中被赋予了名称。这些实体被称为别名。是的,拼写与所示完全一致,因为重音不能在编程语言中使用。目标是创建一个更适合我们记忆的表示,或者使代码更容易阅读。不仅是为写它的人,也是为其他程序员。

亲爱的读者,为了使这一点更清楚,请考虑以下几点。假设你正在编程,为了表明你的代码成功执行了一个操作,你想返回一个 true 值。你会怎样做呢?

好吧,你可以使用一个大于零的值,甚至可以写单词 true。两者在理论上都是有效的。也就是说,代码 07 第 6 行中的值可以写成如下所示。

//+------------------------------------------------------------------+
#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
int OnInit()
{
   return true;
};
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
   return rates_total;
};
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    Print(reason);
};
//+------------------------------------------------------------------+

代码 08

但是,如果您这样做,您的代码将会失败。但为什么它会失败呢?这没有任何意义 — 毕竟,通过使用值 true,我们表明 OnInit 已成功完成。是的,亲爱的读者,当我们返回 true 时,我们确实表示某些操作执行得没有错误。然而,即使如此,你的代码也会失败,并且在 OnInit 执行后立即终止。原因在于与别名 INIT_SUCCEEDED 关联的值。

这并不是说你不能在那里使用其它值,但是你需要明白 INIT_SUCCEEDED 只是底层值的更易读的表示。如果您检查文档,您会发现 INIT_SUCCEEDED 实际上对应的是 0。在布尔逻辑中,0 代表 false。因此,如果您返回 true,则代码 08 肯定会失败,即使其中没有实际错误。

重要细节:如果你执行代码 08,你将看到执行后立即在终端中打印以下消息。


图 06

我理解这目前可能没有多大意义,但我向你保证,假以时日,这会的。您可以在代码 08 的第 6 行中使用值 0 或 false,而不是 INIT_SUCCEEDED,这样就可以完美运行,让指标保持运行,直到您将其从图表中移除。然而,这并不是处理这种情况的唯一方法。您还可以使用如下所示的方法:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. int OnInit()
05. {
06.    return ERR_SUCCESS;
07. };
08. //+------------------------------------------------------------------+
09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
10. {
11.    return rates_total;
12. };
13. //+------------------------------------------------------------------+
14. void OnDeinit(const int reason)
15. {
16.     Print(reason);
17. };
18. //+------------------------------------------------------------------+

代码 09

在代码 09 所示的情况下,就像在代码 08 的第 6 行中使用值 0 或 false 时一样,一旦指标从图表中移除,您将获得下面显示的结果。


图 07

请注意,没有严格的规则规定必须如何编写一段代码,只有你应该理解的指导方针,以确保一切正常运行。现在,让我们假设你是一个非常细致的程序员,不会容忍应用程序中的任何错误。在这种情况下,您可以使用下面显示的代码:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. int OnInit()
05. {
06.    return GetLastError();
07. };
08. //+------------------------------------------------------------------+
09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
10. {
11.    return rates_total;
12. };
13. //+------------------------------------------------------------------+
14. void OnDeinit(const int reason)
15. {
16.     Print(reason);
17. };
18. //+------------------------------------------------------------------+

代码 10

这将是一个极端情况,因为如果在执行 OnInit 函数的任何时候发生错误,返回的值将不同于零或前面提到的指示零值的标签。这是因为我们之前讨论过的 GetLastError 将显示 _LastError 包含一些值。但是,您必须明白,错误可能发生在代码中的任何地方,无论它是否是您编程的一部分。有时,错误的发生不是由于代码本身的失败,而是由于值之间的交互。由于各种原因,这可能会导致 _LastError 保持非零值。这就是为什么这种类型的错误处理被认为是极端的。很少看到实际使用这种方法的代码。但如果你想尝试一下,你会发现意想不到的、有时很有趣的事情可能会发生。

这样做将帮助你变得成熟,并对处理意外情况有更深入的理解。但是,亲爱的读者,请谨慎并耐心地进行。由于我们还没有介绍如何过滤错误,即使是最小的细节也会让你完全困惑,想知道为什么一个应用程序有时能正常工作,而其他时候却不能,没有任何明显的原因。

现在,作为本文的最后一个主题,让我们看看在 MQL5 中使用内部定义的常量和枚举会如何影响我们的工作。正如我们前面讨论的,通过使用 SetUserError 函数,我们可以为系统变量 _LastError 分配任意值。但是如果我们想确切知道 SetUserError 设置了什么值怎么办?这其实很简单。SetUserError 的 MQL5 文档解释了如何对此进行调整。部分文字如下:

将预定义变量 _LastError 设置为等于 ERR_USER_ERROR_FIRST + user_error 的值。

换句话说,如果我们从 _LastError 中的值中减去 ERR_USER_ERROR_FIRST ,我们就可以确定 SetUserError 设置的确切值。以下是代码中的示例实现:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(GetLastError());
07.     SetUserError(2);
08.     Print(GetLastError());
09.     Print(GetLastError() - ERR_USER_ERROR_FIRST);
10. }
11. //+------------------------------------------------------------------+

代码 11

执行代码 11之后,我们可以在终端上看到以下内容:


图 08

请注意,显示了三个值。然而,我们对第二和第三个感兴趣,因为在这种情况下,精确地执行值校正,以确定 SetUserError 过程分配了什么值来指示错误。


最后的探讨

关于这里显示的内容,我有一个小提示。您可能会认为,如果您在 SetUserError 中使用负值,您将能够达到 MQL5定义 的错误值范围来报告一些特殊情况。然而,负值不会产生任何影响。这与 SetUserError 过程所需的数据类型有关。

这些情况将在下一篇文章中加以探讨和解释。至于这里给出的代码,其中一些可以在附件中找到。使用它们自己研究本文中介绍的所有内容。祝您学习愉快,下一篇文章再见!

本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15304

附加的文件 |
Anexo.zip (2.11 KB)
MQL5集成:Python MQL5集成:Python
Python是一种广为人知且流行的语言,具有许多功能,尤其是在金融、数据科学、人工智能和机器学习领域。Python也是一种强大的工具,可以在交易中发挥作用。MQL5允许我们将这种强大的语言作为集成工具,以高效地实现我们的目标。在本文中,我们将在了解一些Python的基本信息后,分享如何在MQL5中使用Python作为集成工具。
威廉·甘恩(William Gann)方法(第一部分):创建甘恩角度指标 威廉·甘恩(William Gann)方法(第一部分):创建甘恩角度指标
甘恩理论的精髓是什么?甘恩角度是如何构建的?我们将为MetaTrader 5创建甘恩角度指标。
解构客户端交易策略的示例 解构客户端交易策略的示例
本文使用框图来检查位于终端的 Experts\Free Robots 文件夹中的基于烛形的训练 EA 的逻辑。
您应当知道的 MQL5 向导技术(第 25 部分):多时间帧测试和交易 您应当知道的 MQL5 向导技术(第 25 部分):多时间帧测试和交易
默认情况下,由于组装类中使用了 MQL5 代码架构,故基于多时间帧策略,且由向导组装的智能系统无法进行测试。我们探索一种绕过该限制的方式,看看搭配二次移动平均线的情况下,研究运用多时间帧策略的可能性。