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

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

MetaTrader 5示例 | 17 二月 2025, 10:25
399 0
CODE X
CODE X

概述

这里呈现的材料仅供教学目的。涉及的应用不应被用于学习和掌握所介绍概念之外的任何目的。

本文的目的是帮助并解释一些用于各种活动的MQL5的概念和用法。它不仅仅专注于创建指标、脚本或EA。这里的主要目标是知识传递。

许多读者已经从我的其他文章中认识了我。然而,由于那些文章主要面向有一定经验的程序员,而许多人也表达了想要更深入地了解这一主题的兴趣,因此我觉得很有必要再创建一个系列,提供一些基础的解释。这并不意味着我们会探讨一些众所周知的话题,让文章变得枯燥无味。相反,我会尽力提供确实有趣且引人入胜的素材。既然还有很多面向其他方向的文章等待我发表,现在我有机会在这个新系列中分享我的知识。我们将涵盖从基础到我认为中级水平的相关问题。我们将学习MQL5中的概念,其中一些概念对于想要学习其他编程语言的人来说也是有用的。

我们先从编程中最基础、最根本的部分开始。现在,我们来谈谈变量。由于我喜欢把事情分得清清楚楚,所以我们将从一个新话题开始这篇文章。


变量——编程基础

许多人错误地认为计算机程序是基于函数和方法构建的。实际上这种假设是错误的。计算机程序大多是基于变量构建的。没有程序是为了其他目的而创建或提供的。其目的始终是使变量可知且可用。

当然,这种观点可能对读者来说显得有些奇怪,尤其是当所有的课程和书籍都倾向于关注函数和方法,暗示这些是编程的基本原则时。然而,我不是来制造争议或分歧的。我是来尽自己的力量做贡献的。虽然被称为函数和方法的术语非常重要,但它们也不过是一种计算过的变量。这将在后续文章中变得更加清晰。

在第一篇文章中,我想向你们介绍变量的概念。是的,变量之间是有区别的。了解每种类型的变量是什么以及如何最好地使用它们,是编写一个优良程序与编写一个仅完成特定任务的简单程序之间的区别。

我们先从这样一个事实开始:变量有两种不同的类型:一种是可以改变值的,另一种是不能改变值的。在第二种情况下,这种变量被称为常量。然而,由于运行在用户模式下的计算机程序总是存于RAM中,所以“常量”这个词并不太合适。原因是,由于值存储在RAM中,它可以通过某种方式被修改。谁没有尝试过用破解工具来绕过某些限制呢?在许多情况下,将破解工具直接作用于可执行文件的某个部分,无论是 RAM中还是磁盘上。

当干预发生在磁盘上时,破解工具直接作用于常量之上。这是因为磁盘上呈现的值不会随着时间的推移而改变。而倾向于保持不变。因此,破解工具实际上作用于一个看似恒定的值。但它是可变的,正是因为破解工具能够修改它。然而,当涉及到 RAM 时,某些部分可能被视为常量,试图改变这样的值将被认为是出错。处理器必须通知操作系统,以便它采取必要的措施。所以,首先要注意的是,变量可以是常量,也可以不是。如果值在 RAM 中,我们的应用程序可以决定什么是常量,什么是变量。而在任何其他情况下,变量都不应被视为常量。

做了这个小小的澄清之后,我们可以进入下一步,将同样的概念付诸实践。我们先从简单之处开始。有些概念很难区分,所以一开始可能会显得相当复杂。请耐心一点,亲爱的读者们。很快您就会更有信心,觉得做各种事情都能得心应手。让我们一步一步地学习,从下面的内容开始。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     value1;
07.     char    value2;
08.     long    value3;
09. 
10.     value1 = 10;
11.     value2 = 5;
12.     Print("Result: ", value1 + value2 + value3);
13. }
14. //+------------------------------------------------------------------+

代码 01

这段代码非常简单且基础。在这里,我们在第06行、第07行和第08行有三个变量声明。目前还不需要担心类型的问题。我们会在另一篇文章中更详细地讨论这个话题。现在,只需关注正在解释的内容。尽管这里声明了三个变量,但其中两个实际上是临时常量,而另一个没有被引用,或者用正确的术语来说,没有被初始化。您可能经常认为这段代码会产生某种结果。但如果不了解不同事物的工作方式,或者忽略它们,可能会使您的整个工作陷入风险。

为了理解这一点,让我们看看编译器的输出。如果您使用的是MetaEditor,结果将如图01所示。


图例 01:

在我的例子中,我使用另一种工具来编辑代码,但它也使用 MetaTrader 5 编译器来创建应用程序。输出如下:


图例 02

请注意,在这两种情况下,编译器都返回了一个警告,我们应该小心对待任何出现的警告。因为通常一个警告表明我们的应用程序可能会以一种意想不到的方式运行。如果我们需要展示与编译器输出相关的某个要点,我们将按照图 02 中展示的模型进行。然而,结果将与我们在使用 MetaEditor 时看到的消息非常相似。所以请仔细阅读,以便您能跟上后续所有文章的内容。

现在假设我们忽略了这个编译器警告。有些警告可以忽略,而有些则不能。我们决定在MetaTrader 5中运行这个已经编译好的应用程序。那么您期望得到怎样的结果呢?您可能期望得到值15,因为在第10行我们说变量 value1等于10,而在第11行我们说变量 value2等于5。所以当第12行被执行时,预期的结果将是这两个值的和,对吧?让我们看一下。结果显示如下:


图例 03

那么这里到底发生了什么?这真的是可能的吗?!是的,亲爱的读者,这是可能的。 您可以亲眼看到这一点。但您的困惑是完全合理的,因为显然您期望打印出的值是15。为什么会出现零值呢?原因就在于value3变量。由于它参与了计算,但没有被正确初始化,因此它可以包含任何值。根据这个值——在编程中我们称之为“垃圾值”——它会使最终结果出错。

在这种情况下,我们有两种选择。第一种是正确初始化value3。第二种是从第12行执行的计算中移除value3。由于我们目前的目标是得到一个等于15的结果,我们将选择第二种方案。因此,在代码01中,我们将第12行修改如下。

Print("Result: ", value1 + value2);

在这种情况下,当您尝试编译代码时,编译器将生成以下警告。


图例 04

请注意,警告与我们之前看到的有所不同。我们分析后决定,在这种特定情况下,它可以被忽略。我们在MetaTrader 5中启动应用程序,并得到了如下所示的结果:


图例 05

它奏效了!我们得到了我们预期的结果。现在让我们回到代码01,来理解一些基本概念。正如前面提到的,变量value1和value2可以并且应该被视为临时常量。为什么呢?原因就在于它们在整个代码中不会改变其值,它们始终具有相同的值。然而,这里有一个问题,事情开始变得有点有趣了。

尽管value1和value2以及value3都是变量,但作为程序员的我们是可以改变这一点的。通过这种方式,我们可以确保分配给这个变量的值不会改变,无论我们在编写代码时可能会犯什么错误。许多处理更复杂、更精细代码的资深程序员更倾向于使用允许这种控制的编程语言。在这种情况下,作为程序员,你们可以说:“听着,编译器,这个变量不应该被视为变量,我希望它是常量。”编译器会帮助你们确保这个变量被存储为常量。为了在MQL5中实现这种控制,我们使用一个保留字段。因此,可以以不同的方式编写出与之前相同结果的代码。但在我们实际操作之前,让我们先看看其他内容。最终,我们构建了如下代码:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const int   value1;
07.     char        value2;
08. 
09.     value1 = 10;
10.     value2 = 5;
11.     Print("Result: ", value1 + value2);
12. }
13. //+------------------------------------------------------------------+

代码 02

如果您尝试编译代码02,编译器会如下报错。


图例 06

在这种情况下,我们遇到了两个错误:一个出现在第六行,另一个出现在第九行。现在让我们来处理这些错误,因为最重要的是不仅要纠正它们,还要理解它们告诉了我们什么。这是因为错误本身并不一定就是错误。很多时候,这可能只是一个简单的编译器警告,提示您正在尝试做一些不应该做的事情。第九行的错误正是这种情况。

请注意这一点,这在MQL5编程中可以极大地帮助您。当我们把一个变量定义为常量时,任何试图在代码运行时改变其值的行为都将被视为一个严重错误。我们还没有启动我们的应用程序。尽管如此,MQL5 编译器已经警告我们,在执行过程中可能会出现严重错误。正因为如此,它不允许代码02被编译。这是因为我们告诉编译器,变量value1不是一个变量,而是一个常量。为此,在代码02的第六行中,我们在操作符之前使用了属性词const。许多经验不足的程序员会倾向于从第6行的声明中移除单词const。这不是错误,但它确实移除了编译器在防止value1变量被当作常量处理时提供的帮助。反过来,这可能会导致应用程序中的其他问题,比如在某些时候生成错误的值,而您却不知道为什么代码没有按预期运行。

但还有另一个问题,正是这个问题导致了第六行的错误,如图06所示。这个问题导致代码01在运行时生成了一个错误的值,当时value3变量被用于代码01中第12行的计算。

问题就在于值的初始化。有些语言会初始化变量的值,通常默认为零。然而,在这种情况下,编译器开发者必须小心,确保被当作常量处理的变量没有明确定义的值。随意且隐式地为任何未初始化的变量分配零值,可能会对最终的应用程序产生不利影响。这正是代码02中展示的细节:如果编译器自动为每个未初始化的变量分配零值,那么第11行操作的结果将是完全错误的。而作为程序员的您,将会花费大量时间试图弄清楚为什么。所以,错误并不总是错误。同样,编译器警告也不总是需要被修复。在某些情况下,我们可以忽略它们所指示的一些信息。

好吧,现在我们已经澄清了这一点,那么我们应该如何编写代码02,以便它可以被编译呢?考虑到我们确实希望事情是这样的,代码应该像下面这样修改。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const int   value1 = 10;
07.     char        value2;
08. 
09.     value2 = 5;
10.     Print("Result: ", value1 + value2);
11. }
12. //+------------------------------------------------------------------+

代码 03

现在一切就绪。编译代码 03。结果如图05所示。然而,您可能会有疑问:为什么非要按照作者展示的那样编程呢?难道不能用其他方法吗,比如下面的代码?

1. #property copyright "Daniel Jose"
2. //+------------------------------------------------------------------+
3. void OnStart(void)
4. {
5.     Print("Result: ", 10 + 5);
6. }
7. //+------------------------------------------------------------------+

代码 04

好吧,在某些情况下,编写像代码04这样的代码实际上是最简单的,因为我们只处理常量。然而,这种方法并不总是可取的。这是因为数值常量是“冷冰冰的”,它们不包含任何关于它们为何存在的信息。看看代码04,您能说出使用值10和5的原因吗?也许我们正在进行某种因式分解,以发现一个最初未知的结果。如果是这样,我们会使用另一种类型的变量,我们将在另一篇文章中详细讨论。然而,如果不是这样,我们最终会怀疑为什么要做这样的计算,我们可能会发现自己很难理解自己的代码。

正因如此,许多程序员会利用他们所使用的编程语言的某些特性。在这种情况下(因为我们刚刚开始讨论这个主题,因此解释如何更高效地编程),使用变量是目前最好的选择。这是因为我们可以给变量一个合适的名字,即使过了很长时间,我们仍然能够理解这个值的用途,即使这个变量是以常量的形式实现的。

我相信第一点已经解释清楚了。读者现在可以理解为什么在某些情况下我们说一个变量是常量,而在其他情况下则不是,以及这样的声明对我们的变量有什么影响。我们还有机会再次讨论这个主题,所以现在我们可以继续讨论变量的基本概念中的另一个方面。


生命周期和可见性

一个常常让即使是经验丰富的程序员也感到困惑的问题是变量的生命周期以及它们的可见性范围。虽然这两个问题看似不同,但可以一起讨论,因为从某种意义上说,生命周期和可见性是相关的。可见性是一个更广泛的话题,我们会在后续的文章中更详细地讨论,因为存在一些可见性的问题,它们对应用程序的设计有着重大影响。我们会适时澄清这些问题。

目前,我们将重点关注最重要的事情:变量何时“诞生”,在哪里“消亡”,以及它的“消亡”是否可以被阻止。

为了避免复杂化问题,我们先做一些考虑。第一个也是最简单的一个是:

变量在其被声明的那一刻“诞生”。

这个说法似乎很显而易见,但从变量可见性的角度来看,事情会变得稍微复杂一些。在深入探讨这个话题之前,我们先来看一个简单的情况。为此,我们使用以下代码:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const int value1 = 10;
07.     int value2;
08. 
09.     value2 = 5;
10.     {
11.         int value2;
12. 
13.         value2 = 8;
14. 
15.         Print("Result #01: ", value1 + value2);
16.     }
17.     Print("Result #02: ", value1 + value2);
18. }
19. //+------------------------------------------------------------------+

代码 05

在执行代码05之前,您能回答在执行第15行和第17行时,终端会打印出什么值吗?这段代码甚至无法编译吗?在您开始怀疑之前,请相信我,这段代码是可以编译并运行的。然而,这里有一个要点是您必须理解的。这是您在学习过程中将会逐渐理解的几个要点中的第一个。

当执行代码05时,您会看到如下图所示的结果。


图例 07

现在我再次问您:您理解这些结果吗?这只是一个小演示,展示了我们真正能做的事情。许多程序员,即使是经验丰富的程序员,也会避免在同一个代码块中给变量赋予相同的名称。但在这里,我们使用的不是一个代码块,而是两个。但这怎么可能呢?代码 05 中我们真的有两个代码块吗?亲爱的读者,我还没有失去理智。重点是我们刚刚触及了一个将在以后某个时刻变得清晰的话题。但在MQL5中,就像在其他类似 C 和 C++ 的语言中一样,每个代码块都以一个左大括号 { 开始,并以一个右大括号 } 结束。

这两个字符之间的所有内容都应被视为一个代码块。还有其他定义代码块的方式,比如循环。然而,使用大括号的这种方式是最容易理解的。因此,在这段代码中,一个代码块从 5行开始,到第18行结束。另一个代码块从第10行到第16行。通过理解这一点,我们可以同时解决两个问题。

第一个问题是:在代码中声明的变量在其被声明的那一刻开始“生命”,并在它所属的代码块结束时结束“生命”。可能会有例外,但不用担心,暂时先不要纠结于此。试着理解我们现在正在处理的这个简单概念。第二个问题是:在代码块之外声明的任何内容在代码块内部都将被视为全局的。

第二个问题稍微难理解一些,但我们来逐步分解。首先,我们来看看为什么代码05会产生图07所示的输出。从上述声明中,您可以看到第7行和第11行声明了一个同名的变量。在第9行和第13行,我们给理论上似乎是同一个变量赋予了不同的值。然而,如果您仔细观察代码,会发现它们位于不同的代码块中:一个在外部代码块中,另一个在嵌套在第一个代码块内部的代码块中。

简单来说,我们在第7行声明value2的代码块是全局的。因此,在第7行声明的value2是一个全局变量。而第11行声明的变量value2是一个局部变量。从外部代码块的角度来看,第11行的变量是局部的;但在从第10行开始的代码块内部,第11行声明的变量在那个上下文中被视为全局变量。

我知道这可能会让人感到困惑,所以我们先停下来思考一下。理解这个概念非常重要,因为我们不必总是使用不同的名称。编译器要求我们在同一个代码块中使用不同的名称来声明变量,但如果变量位于不同的代码块中,编译器会认为它们是具有相同名称的不同变量。

那么,为什么最终结果中我们得到了两个不同的值呢?原因很简单:如果局部变量和全局变量具有相同的名称,无论变量的类型如何,局部变量都会优先于全局变量。如果名称相同,编译器会优先使用局部变量。但请注意,其他一些语言可能会使用不同的技术。我们在这里看到的是特定于MQL5的,因为我们专注于这种语言。

因此,很容易理解以下事实:当我们说在第13行 value2是8时,它并不会改变全局作用域中value2的值,这个值是5,是在第9行定义的。因此,当我们使用第15行打印value1和value2的和时,结果是18。而当第17行执行相同的计算时,结果将是15。

然而,如果您尝试使用以下代码做类似的事情:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const int value1 = 10;
07.     int value2;
08. 
09.     value2 = 5;
10. 
11.         int value2;
12. 
13.         value2 = 8;
14. 
15.         Print("Result #01: ", value1 + value2);
16. 
17.     Print("Result #02: ", value1 + value2);
18. }
19. //+------------------------------------------------------------------+

代码 06

编译器会报错,并生成类似于下面图 08 所示的内容。


图例 08

这是因为与代码05不同,代码06只有一个从第5行开始、到第18行结束的代码块。因此,在编写代码或试图理解另一位程序员的代码时,请务必小心,这也可能是您尝试修改代码却失败的原因。

然而,您可能会遇到一种稍有不同的情况。以下代码中的例子就展示了这种情况。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. int value2;
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     const int value1 = 10;
09. 
10.     value2 = 5;
11. 
12.         int value2;
13. 
14.         value2 = 8;
15. 
16.         Print("Result #01: ", value1 + value2);
17. 
18.     Print("Result #02: ", value1 + value2);
19. }
20. //+------------------------------------------------------------------+

代码 07

在这种情况下,编译器将输出一条与图 09 中看到的消息非常相似的信息。


图例 09

查看图09中显示的消息,如果我们的目标是获得类似于图07中所示的结果,那么针对代码07应该采取什么措施呢?亲爱的读者们,现在一切都变得更加复杂了。原因是,在第4行声明的value2只有在应用程序停止运行或不存在时才会“消亡”。因此,在这种情况下,您不能使用具有局部作用域的变量与具有全局作用域且名称相同的变量。

一般来说,全局变量或值本身是一个应该尽量避免的问题。这并不总是可能的,但如果出现这种情况,或者没有其他选择,您应该为变量使用不同的名称。许多程序员,包括我自己,通常会添加前缀或后缀来区分全局变量(它们将存在于程序的整个生命周期中)和局部变量,甚至是属于特定代码块的全局变量。


最后的思考

第一篇文章到这里就结束了。在这篇文章中,我试图向您解释,无论您是初学者还是偶尔编程的人,如何更正确、更高效地使用 MQL5。这篇文章的目的是与您分享我作为一名专业程序员的经验,向您展示如何轻松、有效地开始编写自己的代码,而无需不必要的麻烦。

编程是一件美好且难忘的事情。只要方法得当,并且拥有扎实且连贯的知识,它肯定会在未来为您带来良好的结果。因此,在下一篇文章中,我们将继续讨论变量,因为在这里我只介绍了我们在后续文章中将要使用的具体方法。希望您喜欢这篇文章。如果您还想阅读更复杂的内容,但采用类似的方法论,可以阅读我在本社区发布的其他文章,尽管它们是用另一个身份发布的。


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

附加的文件 |
Anexo.zip (0.73 KB)
结合基本面和技术分析策略在MQL5中的实现(适合初学者) 结合基本面和技术分析策略在MQL5中的实现(适合初学者)
在本文中,我们将讨论如何将趋势跟踪和基本面原则无缝整合到一个EA中,以构建一个更加稳健的交易策略。本文将展示任何人都可以轻松上手,使用MQL5构建定制化交易算法的过程。
您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试 您应当知道的 MQL5 向导技术(第 21 部分):配以财经日历数据进行测试
默认情况下,财经日历数据在策略测试器中不可用于智能系统测试。我们看看数据库能如何提供帮助,绕过这个限制。故此,在本文中,我们会探讨如何使用 SQLite 数据库来存档财经日历新闻,如此这般,由向导组装的智能系统就可以用它来生成交易信号。
从基础到中级:变量(II) 从基础到中级:变量(II)
今天,我们将探讨如何使用静态变量。这个问题常常让许多程序员感到困惑,无论是初学者还是有一定经验的开发者,因为使用这一机制时需要遵循一些特定的建议。本文旨在为教学目的提供材料。在任何情况下,应用程序都应仅用于学习和掌握所介绍的概念。
适应性社会行为优化(ASBO):两阶段演变 适应性社会行为优化(ASBO):两阶段演变
我们继续探讨生物体的社会行为及其对新数学模型 ASBO(适应性社会行为优化)开发的影响。我们将深入研究两阶段演变,测试算法并得出结论。正如在自然界中,一群生物体共同努力生存一样,ASBO 使用集体行为原理来解决复杂的优化问题。