
从基础到中级:变量(II)
引言
本文旨在为教学目的提供材料。在任何情况下,应用程序都应仅用于学习和掌握所介绍的概念。
在上一篇文章《从基础到中级:变量(I)》中,我们开始讨论变量及其相关方面。例如,我们探讨了如何将变量转换为常量。我们还讨论了变量的生命周期和可见性。
在这里,我们将继续这一主题,假设读者已经正确理解了之前的材料。当谈到变量的生命周期和可见性时,对于初学者来说可能会有些难以理解。原因在于,很多时候我们不希望全局变量带来不便。我们希望变量只存在于一个代码块内。然而,这里的情况变得复杂——我们不希望变量的值在代码块结束时消失。
这种情况在许多程序员(包括希望成为专业人士的初学者)的脑海中是最令人困惑的。这是因为许多人没有意识到,一些编程语言有机制可以让变量在内存中保留其值。这种复杂性可能是因为流行的脚本语言(如Python)没有使用这种实现。因此,对于习惯使用Python的程序员来说,理解这个概念非常困难。当一个变量所属的代码块不再存在时,变量并不总是丢失或忘记其值。
然而,C/C++程序员通常对这个概念非常熟悉,因为它深深植根于他们的经验中。由于MQL5与这些语言相似,它也采用了这一原则——在我看来,这是一个非常有益的特性。如果没有它,开发人员将不得不依赖于应用程序范围内的全局变量,这通常是一个很大的不便。
现在,为了说明这一点,让我们开始一个新的主题。
静态变量
静态变量是许多初学者程序员最难理解的概念之一,但同时也是最强大的工具之一。乍一看,它们似乎违反直觉。然而,当你开始研究它们并开始理解它们的工作原理,特别是它们何时以及在哪里可以被应用时,你可能会深刻地欣赏它们的实用性。它们解决了许多否则需要更多努力才能解决的问题。
让我们从一个相对简单的案例开始。尽管我们还没有深入某些编程概念,但我鼓励你现在只关注变量。忘记其他一切。试着理解变量发生了什么,因为这是这里的关键点。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. uchar counter = 0; 07. ulong value = 1; 08. 09. Print("Factorial of ", counter, " => ", value); 10. counter = counter + 1; 11. value = value * counter; 12. Print("Factorial of ", counter, " => ", value); 13. counter = counter + 1; 14. value = value * counter; 15. Print("Factorial of ", counter, " => ", value); 16. counter = counter + 1; 17. value = value * counter; 18. Print("Factorial of ", counter, " => ", value); 19. counter = counter + 1; 20. value = value * counter; 21. Print("Factorial of ", counter, " => ", value); 22. } 23. //+------------------------------------------------------------------+
代码 01
我知道我们这里可以使用一个循环。然而,让我们假设你正在从零开始学习编程。你对这个主题一无所知,而到目前为止你所学到的一切都是基于上一篇文章。鉴于此,我们需要从头开始。一开始可能会感觉有点复杂,但很快情况就会好转。
这里的关键细节是:当我们运行代码01时,结果将如下所示。
图例 01
正如你所见,它奏效了。我们在这里计算一个数字的阶乘。但与其简单地计算结果并返回,我们将其分解为逐步计算。这种方法对于理解我想要表达的观点至关重要。
现在,亲爱的读者,请考虑以下内容:在第六行,我们创建并初始化了一个变量。这个变量将跟踪当前正在计算的阶乘。然后,在第七行,我们声明了另一个变量并用适当的值初始化它。是的,0的阶乘是1,1的阶乘也是1。了解这一点对于任何编程任务都至关重要,因为编程涉及数学问题的解决。,因为编程涉及数学问题的解决。]与许多人认为的相反——编程只是随意输入命令并让计算机执行它们,编程的本质是将数学公式转化为计算机能够理解的代码。因此,扎实的数学基础对于成为一名优秀的程序员至关重要。
回到我们的问题,在第09、12、15、18和21行,我们将结果以人类可读的形式输出。在这些步骤之间,我们指导机器如何计算下一个值。现在,请注意这个关键点:每次我们计算当前数字的阶乘时,我们使用前一个阶乘的值。换句话说,尽管变量counter和value最初是常量,但在每次新的计算中,这些常量值会发生变化,实际上变成了新的常量。这是作为程序员的你,在你的职业生涯中应该始终牢记的关于变量的最基本概念之一。
既然我们已经达成共识,我们可以通过减少代码行数来简化事情。为了实现这一点,我们需要使用一个稍后会更详细解释的概念,但在这个阶段是必要的。这个概念就是例程。
例程本质上是机器在我们指示时会执行的任务。它可以被视为一种重复且繁琐的任务,我们不想每次都重新编写代码。这与代码01中的几行代码类似。例程主要有两种类型:函数和方法。然而,为了讲解静态变量,我们将使用方法,因为它提供了一种更简单的方式来理解发生了什么。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. uchar counter = 0; 05. ulong value = 1; 06. //+------------------------------------------------------------------+ 07. void OnStart(void) 08. { 09. Factorial(); 10. Factorial(); 11. Factorial(); 12. Factorial(); 13. Factorial(); 14. } 15. //+------------------------------------------------------------------+ 16. void Factorial(void) 17. { 18. Print("Factorial of ", counter, " => ", value); 19. counter = counter + 1; 20. value = value * counter; 21. } 22. //+------------------------------------------------------------------+
代码 02
请注意,代码02更容易理解。乍一看,很明显我们正在计算从零到四的阶乘值。这是因为我们在代码的主块(即OnStart方法)中有四次对Factorial方法的调用。正如前面提到的,我们稍后会详细讨论这一点。但目前,让我们专注于理解变量。
尽管代码01和代码02在表面上有所不同,但两者的结果都是图01。然而,在代码02中,我们使用了两个全局变量,它们在代码执行时出现,并且只有在代码执行完成后才会消失。在某一时刻和下一时刻之间,这些变量可能会被任何有访问权限的方法或函数更改。即使这种更改是由于我们在实现代码时的错误或疏忽造成的。在复杂的代码中,这种情况并不少见。
尽管这些变量是全局的,并且不属于任何特定的代码块,但这些值仍然需要初始化。正如你所见,我们在声明它们时就进行了初始化。然而,这可以在其他任何时间完成,这使得跟踪它们值的变化变得困难。但为了简单起见,我们在声明它们时就进行了初始化。此时,一个完全合理的问题出现了:如果我正在设计一个复杂的程序,并且担心在编码过程中不小心修改全局变量的值,那么将变量counter和value声明在Factorial方法内部是否更好呢?这样一来,我们就可以避免无意中更改这些变量。
是的,亲爱的读者,最好的方法确实是将counter和value从全局作用域中移除,并在Factorial方法内声明它们。这样做将使我们对代码中的变量有更好的控制。那么,让我们做出这个调整,看看会发生什么。乍一看,代码01和代码02似乎都按预期工作。如果它们能够正常工作,那就意味着我们正朝着正确的方向前进。通过实施提议的更改,更新后的代码将如下所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Factorial(); 07. Factorial(); 08. Factorial(); 09. Factorial(); 10. Factorial(); 11. } 12. //+------------------------------------------------------------------+ 13. void Factorial(void) 14. { 15. uchar counter = 0; 16. ulong value = 1; 17. 18. Print("Factorial of ", counter, " => ", value); 19. counter = counter + 1; 20. value = value * counter; 21. } 22. //+------------------------------------------------------------------+
代码 03
太棒了!现在,我们的代码不再有全局变量的问题。我们的程序确实更加有条理了。但是结果呢?我们是否仍然在计算从零到四的阶乘值?让我们看看。在编译代码并在MetaTrader 5终端运行后,显示了以下结果:
图例 02
等等。。。这行不通。但为什么呢?之前还执行的好好的。现在我真的很困惑,因为乍一看,似乎我们按预期执行了五次代码。然而,看起来我们总是指向相同的值。嗯,让我想一会儿。稍等。
啊,现在我明白了!每次在主块(OnStart方法)中执行一次调用时,变量counter和value都会被重新声明。而且每次我们这样做时,它们都会被重新初始化为代码中先前定义的初始值。这就是为什么它不起作用的原因!这帮助我理解了变量生命周期的概念。但我还有一个问题:如果我不在第15行和第16行初始化这些值呢?也许那样就能奏效!不,亲爱的读者,你可以尝试这样做,但你会遇到我们在上一篇文章中讨论过的问题。
那么,我们可以将值传递给方法吗?那样可以奏效。是的,在这种情况下,这将解决这个问题。然而,这种方法为时过早。因为我还没有解释如何在过程和函数之间传递值,所以你暂时还不能使用这种技术。你需要想出另一种方法,让代码03能够正常工作,并显示出与图01相同的结果,而不是我们在图02中看到的奇怪结果。你是不是要放弃了?
如果是这样的话,那么是时候结束你的挫败感了。为了在不使用任何“神奇修复”的情况下解决这个问题,并且保持变量counter和value在Factorial过程内部,我们需要使用一种被称为静态变量的东西。当一个变量被声明为静态时,它在对同一个过程或函数的不同调用之间不会丢失其值。这听起来复杂吗?一开始可能会,但在实践中,它比看起来简单得多。只要你注意你在做什么,一切都会顺利。然而,如果你粗心大意,事情可能会迅速失控。为了更好地理解这一点,让我们看看实际中的代码是什么样的。更新后的版本如下所示:当一个变量被声明为静态时,它在对同一个过程或函数的不同调用之间不会丢失其值。
这听起来复杂吗?一开始可能会,但在实践中,它比看起来简单得多。只要你注意你在做什么,一切都会顺利。然而,如果你粗心大意,事情可能会迅速失控。为了更好地理解这一点,让我们看看实际中的代码是什么样的。这可以在下面看到。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Factorial(); 07. Factorial(); 08. Factorial(); 09. Factorial(); 10. Factorial(); 11. } 12. //+------------------------------------------------------------------+ 13. void Factorial(void) 14. { 15. static uchar counter = 0; 16. static ulong value = 1; 17. 18. Print("Factorial of ", counter, " => ", value); 19. counter = counter + 1; 20. value = value * counter; 21. } 22. //+------------------------------------------------------------------+
代码 04
可以看到,代码03和代码04之间唯一的区别是在第15行和第16行出现了保留关键字static。然而,这一处改动确保了当代码04在MetaTrader 5终端执行时,结果与图01中显示的完全一致。换句话说,我们取得了预期的成功。但由于静态变量的静态特性,它们应该谨慎对待。这是因为它们的行为受到特定规则的约束。虽然你没有严格遵循某些实践的义务,但理解静态变量的工作原理以避免问题至关重要。
静态变量主要是设计为存在于一个代码块内部的。尽管在一些罕见的情况下,它们可以出于特殊目的在代码块外部使用,但你通常应该将静态变量视为与代码块绑定的。
其次,除了非常特定的用例外,你永远不应该在声明之外初始化静态变量。如果不经过适当考虑就这样做,可能会导致对代码失去控制。在这个简单的教学示例中,你可能会觉得自己完全掌控了局面。然而,不要因此就认为这总是如此。一个错误或疏忽可能会迅速导致你花费数小时(甚至数天)来调试问题。为了清楚地说明这一点,让我们对代码04做一个小改动,看看会发生什么。更新后的代码如下所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Factorial(); 07. Factorial(); 08. Factorial(); 09. Factorial(); 10. Factorial(); 11. } 12. //+------------------------------------------------------------------+ 13. void Factorial(void) 14. { 15. static uchar counter; 16. static ulong value = 1; 17. 18. counter = 1; 19. Print("Factorial of ", counter, " => ", value); 20. counter = counter + 1; 21. value = value * counter; 22. } 23. //+------------------------------------------------------------------+
代码 05
我附上了完整的代码,这样你可以更清楚地看到变化。这样,你就能更好地理解在应用程序中出现小错误时会发生什么。请注意并比较代码04和代码05之间的差异。乍一看,这可能看起来微不足道,几乎无害。你不会认为这会影响结果。然而,当执行时,本应在MetaTrader 5终端显示为图01的内容,现在却变成了完全不同的东西,如下图所示:
图例 03
真是奇怪。本应是阶乘计算的,却变成了二的幂计算。这是怎么发生的?好吧,亲爱的读者,正确使用静态变量有一个小窍门。当经验丰富的程序员面对不符合预期的代码时,会很快怀疑是变量使用出了问题。如果语言支持静态变量且代码中包含它们,你应该首先检查这些变量是否正确初始化了
,以及它们的值是否会发生变化。这些是首先要检查的点。既然变量值的变化是我们预期的(否则我们会使用常量),我们可以检查变量是否发生了变化,以及是否忘记了它最后一次被赋予的值。对此有各种调试技巧。我鼓励你详细研究调试方法,因为每种情况可能需要略有不同的方法。
但回到代码表现异常的问题,尽管它看起来没问题。首先想到的是,一切看起来都没错。然而,静态变量在创建时会被初始化。如果错过了这个初始化,变量就会表现得像局部变量一样。这意味着变量每次都会被重新声明,失去了它的静态特性。自然地,这意味着在代码05的第18行中显示的内容应该尽可能避免出现在代码块中。这会导致静态变量失效并出现故障。这就是代码表现异常的原因:原本是静态变量,现在却变成了普通变量。
至此,我们已经为接下来的要点打下了良好的基础。
数据类型
现在,让我们简要谈谈数据类型,因为理解这个概念对你很重要。随着我们继续深入,你会发现涉及的内容更多。
我们这里讨论的主题是数据类型或变量类型。许多编程语言,特别是像JavaScript和Python这样的脚本语言,并不要求程序员明确声明存储的数据类型。这些被称为动态类型语言。另一方面,像C/C++和MQL5这样的语言是静态类型语言。这意味着程序员必须指定每个变量中预期的数据类型。这在实践中意味着什么?在像Python这样的动态语言中,一个变量可以存储任何类型的值,语言会自动将变量调整为适当的类型。然而,在MQL5中,变量不能存储任何值——它必须被声明为一个特定的类型。当然,从理论上讲,有一些方法可以绕过这种不便。
但对于那些使用严格类型语言编程的人来说,有些事情会变得更复杂,因为仅仅声明一个变量并放入任何你想要的东西是不够的。对此有规则和限制。然而,像Python这样的语言允许变量接受文本甚至任何其他值,语言会自动为变量设置最合适的类型。因此,编程的复杂性显著降低,这使得代码的创建变得更加容易、快速,也不需要程序员具备如此多的专业知识。这与静态类型语言不同,在静态类型语言中,我们需要知道每种类型可以或不可以包含什么。
此时,你可能正在考虑放弃理解MQL5,转而寻找更简单的东西,比如Python或JavaScript。然而,一旦你习惯了静态类型语言,你可能会觉得在动态类型语言中工作没那么有趣,因为它们在数据处理方面需要的思考更少。处理数据涉及理解它是什么,如何将其分类为不同类型,以及每种类型的限制是什么。学会处理不同数据类型会为你打开许多机会,比如从事加密或数据压缩工作。
但这又是另一个话题了。现在,让我们先了解一下MQL5中的基本数据类型。
在MQL5中,你可以使用几种基本类型:整数、布尔值、字面量、浮点数、枚举器和字符串。这些类型也出现在C/C++代码中。然而,除了这些在许多基于C/C++的语言中常见的类型外,MQL5还有一些特殊的类型,比如颜色数据和日期时间数据。当然,正如我们稍后会看到的,它们是其他类型的衍生品,但它们的存在在某些时候确实很有用。
C/C++和MQL5中都存在复杂的数据类型。这些是结构体和类。说到类,它们确实存在于C++中。但在MQL5中,类比在C++中更容易理解和使用。尽管作为C++程序员,我有时会怀念C++中存在但在MQL5中不存在的某些类资源。但我们适应了,以便完成工作并实现目标。
这只是对这个主题的基本介绍,但我们可以稍后再深入探讨。在结束这篇文章之前,我们先简要讨论一下基本类型的限制。了解每种类型的限制将在你未来的编程中大有帮助。让我们先从下面的表格开始,它概述了这些类型的限制。
类型 | 字节数 | 最小值 | 最大值 |
---|---|---|---|
char | 1 | -128 | 127 |
uchar | 1 | 0 | 255 |
bool | 1 | 0 (False) | 1 (True) |
short | 2 | -32 768 | 32 767 |
ushort | 2 | 0 | 65 535 |
int | 4 | - 2 147 483 648 | 2 147 483 647 |
uint | 4 | 0 | 4 294 967 295 |
color | 4 | -1 | 16 777 215 |
float | 4 | 1.175494351e-38 | 3.402823466e+38 |
long | 8 | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
ulong | 8 | 0 | 18 446 744 073 709 551 615 |
datetime | 8 | 0 (01/01/1970 00:00:00) | 32 535 244 799 (31/12/3000 23:59:59) |
double | 8 | 2.2250738585072014e-308 | 1.7976931348623158e+308 |
主要类型表
这张表仅展示了简单数据类型。我们在这里不分析复杂类型,因为稍后会专门讨论它们。然而,理解这张简单数据类型的表将帮助我们根据计算需求选择最合适的数据类型。这是因为每种类型都有其能表示的上下限。
然而,表中有两种数据类型我们会在未来更详细地探讨:double 和 float 类型。这是因为它们各自有独特的特性,需要单独解释。
但让我们简要讨论一下这张表。你可以看到类型名称中的相似之处,在某些情况下,字母“u”出现在类型名称之前,例如 char 和 uchar。字母“u”有特殊的含义:当它出现在类型名称之前时,表示该类型从零开始,直到某个上限。因为这个前缀“u”来自“unsigned”(无符号)这个词。但“无符号”在实践中是什么意思呢?当我们有一个无符号值时,它总是被解释为正数。如果一个值是有符号的,那么它可以是正数或负数。当我们讨论数学运算时,这一点会变得更加清晰。一般来说,可以这样理解:如果我们想存储一个负值,我们可以将其放入有符号类型中。然而,如果要存储的所有值都是正数,那么可以使用无符号类型。但为什么我要谈论什么是可能的呢?难道不是应该谈论什么是必要的吗?实际上,这并不是必要的。我们稍后在讨论数学运算时会更详细地考虑这一点。这里有一个基本规则:整数值,即不使用浮点数的值,是精确的。浮点数值是不精确的。在查看这张表时,你可能会认为总是使用 double 数据类型更好,因为它们覆盖的范围更广,但在实践中并非总是如此。因此,一个好的程序员需要知道如何选择正确的数据类型。
然而,你可能在看这张表时会想:string 类型在哪里,它不是一种基本类型吗?是的,“string”是一种基本类型。但它是一种更适合归入另一个类别的特殊类型,我们稍后会讨论。因此,它没有出现在这张表中。
后记
在本文中,我们主要讨论了静态类型变量。我们对它们进行了一些实验,并展示了它们的使用方法。文章重点解释了静态类型的其中一种可能用途,因为这种变量也可以用于其他目的。然而,在大多数情况下,当我们遇到静态类型变量时,它们的使用方式正如这里所展示的。稍后,我们将探讨静态变量用于其他目的的其他方法。目前,你在这里看到的内容将帮助你理解许多程序,并且实现不依赖于过度使用全局变量的更高效解决方案。
你对下一篇文章的主题——数据类型——也有了一个简短的介绍。MQL5 中的每种数据类型都有其差异和限制,但我们还没有探讨这些限制为何存在,以及如何绕过这些限制以扩展它们以满足更具体的目的。
但不用担心,我们很快就会看到。在此,我结束了这篇文章。我将文章中附带的代码保留下来,这样你可以通过实验得出自己对这里解释内容的结论。待会儿见!
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15302
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


