
从基础到中级:数组(三)
概述
在上一篇文章从基础到中级:数组(二)中,我解释了使用动态和静态数组的基础知识、它们之间的区别以及在应用程序中使用数组时应采取的基本注意事项。
我们将继续研究数组的主题。理解本文的前提是很好地理解前一篇文章中涵盖的概念。此外,了解按值传递和按引用传递是如何工作的也很重要。所有这些主题都在前面的文章中进行了讨论和演示。所以,如果你对这些概念有任何疑问或不熟悉,因为你才刚刚开始,我建议你在继续之前花点时间复习一下前面的文章。这将确保您能够按照解释进行操作。
像往常一样,让我们开始一个新的部分来继续我们的研究和演示。
在函数和过程中使用数组
经常困扰初学者的一个主题是使用数组作为在函数和过程之间传递值的手段。在这里,我将直截了当地谈谈关于这个概念的一点。MQL5 处理此问题的方式比其他一些语言处理相同问题的方式简单得多。
C 和 C++ 等语言在这方面非常复杂。在那些情况下,没有 MQL5 中那样的真正数组结构。相反,我们使用另一种称为指针的结构。指针的问题使它们变得复杂且难以掌握,这源于在某些情况下我们可以使用间接引用。虽然这为程序员提供了巨大的能力,但也使代码更难理解。特别是对于那些在编码和指针使用方面经验有限的人。
在 MQL5 中,事情更容易掌握和应用。从一个简单的事实开始:
每个数组总是通过引用传递的,无论是传递给函数还是传递给过程。
在 MQL5 的函数或过程中使用数组作为参数时,没有按值传递选项。
现在,你可能会想:“但是,如果使用数组的每个参数都是通过引用传递的,无论是传递给函数还是过程,这难道不会降低代码的安全性吗?”事实上,不是的,亲爱的读者。听起来可能很奇怪,在 MQL5 中使用数组比使用其他类型的变量安全得多。
事实上,我甚至可以说,在 MQL5 中使用数组比使用其他编程语言中使用的任何其他方法都要容易得多。这是因为,在 MQL5 中,您可以完全控制数组可能发生或不可能发生的事情,即使它总是通过引用传递给函数或过程。
事实上,确保这种安全性和可靠性的是程序员的仔细关注。当我解释如何使用引用传递或值传递时,我强调了每种方法的优缺点。但在这里,它就简单多了。函数或过程的声明就清楚地表明了这一点。
还有第二个关键细节需要考虑。它涉及以下事实:
任何声明为函数或过程的参数的数组必须始终是动态的。
应用这两个原则,您将能够在 MQL5 中创建涉及数组的任何类型的实现。通常,在函数或过程中执行计算比内联代码更实用、更合适。
请注意:对于那些不熟悉“内联代码”一词的人来说,它指的是程序员在应用程序中不使用函数或过程的做法。相反,他们创建了一系列例程,一个接一个,就像遵循一个长长的食谱一样。尽管这种方法在极少数情况下是可行的,但它确实发生了。这种代码的主要特征是在实现中完全没有函数或过程。
现在,让我们回到我们的主要主题:如何在函数和过程中使用数组作为参数。尽管这个过程看起来很简单,但亲爱的读者,你不应该变得不那么小心,认为一切都很简单,没有风险。
事实上,只有当我们付诸实践时,某些细微差别才会变得清晰。这个理论可能看起来很简洁,只需要我们理解几个概念。然而,在实践中,事情可能会变得棘手,在我们设法克服它们之前,偶尔会有所反击。那么,让我们从实际操作中的实际情况开始。让我们从下面显示的代码开始。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. const char Infos[] = {2, 3, 3, 5, 8}; 07. 08. Print("Based on the values:"); 09. ArrayPrint(Infos); 10. PrintFormat("There are %d possibilities.", Possibilities(Infos)); 11. } 12. //+------------------------------------------------------------------+ 13. ushort Possibilities(const uchar &arg[]) 14. { 15. ushort info = 0; 16. 17. for (uchar c = 0; c < arg.Size(); c++) 18. info = (c ? info * arg[c] : arg[c]); 19. 20. return info; 21. } 22. //+------------------------------------------------------------------+
代码 01
当执行代码 01 时,您将看到如下图所示的结果。
图 01
您在图 01 中看到的内容可以通过许多不同的方式生成。代码 01 中使用的方法纯粹用于教育目的。这里的目标正是解释输出中发生了什么,演示如何将数组传递给函数或过程。
那么,让我们来分析一下这里发生了什么。我将只强调我认为在前面的文章中尚未涵盖的部分。让我们从第 6 行开始,深入探讨亮点。虽然我们在那里使用静态数组,但这样做只是为了简化代码。我们本可以在这一行中使用动态数组,在功能方面不会有任何变化。实际目标是将值传递到第 13 行声明的函数中。因此,代码 01 中第 10 行之前发生的任何事情都不会影响数组的传递方式。嗯,只要函数声明如第 13 行所示。
亲爱的读者,现在,请密切关注这里将要解释的内容。在之前的文章中,我们已经看到,声明为常量的数组必须在声明本身中进行初始化。这样做是为了避免编译错误。然而,仔细看看第 13 行。在那里,我们将数组声明为常量。然而,编译器仍然会生成可执行文件。
为什么在这里可以这样声明,但在第 6 行却不行?原因是,在第 13 行,与第 6 行不同,数组不是被声明为变量,而是被声明为参数。正如本主题前面提到的,每个数组都必须通过引用传递。这正是这里正在发生的事情。
你们中的一些人,特别是编程新手,可能会错误地认为第 13 行声明的数组没有因为第 6 行中的数组而被声明为常量。这两者毫无关系。您甚至可以在第 6 行中将数组声明并初始化为完全动态的。然而,这不会改变第 13 行的声明,因为上下文和目的完全不同。
代码 01 中还有另一个细节,我认为值得在这里再次强调,尽管它之前已经提到过。该问题与第 17 行的测试值有关。在那里,我们使用了 arg.Size()。这是允许的并且完全有效的,因为结果与我们使用 ArraySize 函数的结果相同。无论哪种写法,检查都会以相同的方式进行。作为家庭作业,尝试将 arg.Size() 替换为 ArraySize,以便更好地理解代码在实践中的实现方式。
太好了,我相信这个初步的概述相对来说是愉快和愉快的,它让你了解了如何实现代码来传递数组,无论是传递给函数还是过程。在此特定上下文中使用数组并将其声明为参数的方法不会改变。只有目的可能不同。
这是最简单的部分,现在我们可以讨论一些更复杂的事情了。但是,为了让您仔细研究每个主题,我们将在新的部分中介绍这些新的复杂事物。
远程修改数组
在函数和过程中使用数组有点复杂的一个用例是远程修改数组内容的能力。换句话说,您将数组传递给函数或过程,然后在该函数或过程中对其进行修改。这种行为可能会导致非常混乱和复杂的问题需要解决。然而,由于我们可以 —— 在许多情况下也会 —— 需要做这类事情,因此你必须充分了解会发生什么。但首先,重要的是要理解为什么会发生这种情况。
与 char、int、long、double 和类似类型等离散值不同,大多数编程语言不允许我们返回数组,除非在特殊情况下语言本身提供了处理这种情况的机制。MQL5 中的一个这样的例子就是字符串。
如前几篇文章所示,字符串实际上是一种特殊类型的数组。这使得它成为我们可以从 MQL5 中的函数返回数组的少数情况之一。此功能已经消除了在某些实现中可能出现的风险和潜在问题,在这些实现中,我们确实需要返回修改后的数组。
但在我们深入探讨这个主题之前,由于在实际代码中理解这是一个相当复杂的概念,我们需要看看其他语言是如何处理这个问题的。例如,在 C 和 C++ 中,责任完全由程序员承担。在这两种语言中,我们既可以选择修改数组,也可以选择返回一个全新的数组。但是,不要误以为这会让你更不容易出错。事实上,它使你面临更复杂的错误。这就是 C 和 C++ 如此难以掌握的原因之一。
其他语言,如 Python 和 JavaScript,基本上完全绕过了传统数据类型的使用。它们实现了自己的方法,允许我们返回数组,甚至修改数组。这个过程不太常见,因此对于某些类型的重构来说稍微简单一些。尽管如此,我们在这里的重点是 MQL5。
那么,让我们从最简单的例子开始。我们将采用代码 01,只修改一个小细节。这引出了下面要展示的内容。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Infos[] = {2, 3, 3, 5, 8}; 07. 08. Print("Based on the values:"); 09. ArrayPrint(Infos); 10. PrintFormat("There are %d possibilities.\nValue after call:", Possibilities(Infos)); 11. ArrayPrint(Infos); 12. } 13. //+------------------------------------------------------------------+ 14. ushort Possibilities(uchar &arg[]) 15. { 16. ushort info = 0; 17. 18. for (uchar c = 0; c < arg.Size(); c++) 19. info = (c ? info * arg[c] : arg[c]); 20. 21. arg[0] += 2; 22. 23. return info; 24. } 25. //+------------------------------------------------------------------+
代码 02
当我们运行代码 02 时,我们将得到下面显示的输出。
图 02
请注意,在图 02 中,我突出显示了两个值。这正是为了引起你对这里发生的事情的注意。
请注意,代码 02 和代码 01 之间的区别恰恰在于第 14 行函数中的参数声明不再是常数 。因此,总是发生在数组中的引用传递机制允许我们使用第 21 行,在那里我们修改了第 6 行声明的数组中一个元素的值。
由于我们在这里是在教学环境中工作,专注于解释适用于 MQL5 的某些概念,因此这种修改很容易发现。然而,在现实世界的代码中,它可以更深入地嵌入,并且经常纠缠在代码中,这可能会让你想放弃并重新从头开始编码一切,这就是可能出现的复杂性。
这就是为什么,亲爱的读者,我鼓励你,特别是如果你刚刚开始学习编程或经验有限,彻底练习这些概念。只有通过实践,你才能获得处理不可避免出现的问题所需的经验。
好吧,代码 02 确实是最简单的类型。从某种意义上说,这几乎微不足道。然而,随着我们对这一主题的深入研究,事情开始变得更加复杂。那么,让我们来看另一个例子,这次有点复杂。如下图所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Infos[]; 07. 08. ArrayResize(Infos, 7); 09. ZeroMemory(Infos); 10. 11. Infos[0] = 2; 12. Infos[1] = 3; 13. Infos[2] = 3; 14. Infos[3] = 5; 15. Infos[4] = 8; 16. 17. Print("Based on the values:"); 18. ArrayPrint(Infos); 19. PrintFormat("There are %d possibilities.\nValue after call:", Possibilities(Infos)); 20. ArrayPrint(Infos); 21. 22. ArrayFree(Infos); 23. } 24. //+------------------------------------------------------------------+ 25. ushort Possibilities(uchar &arg[]) 26. { 27. ushort info = 0; 28. 29. for (uchar c = 0; c < arg.Size(); c++) 30. info = (c ? (arg[c] ? info * arg[c] : info) : arg[c]); 31. 32. arg[arg.Size() - 1] = 9; 33. 34. return info; 35. } 36. //+------------------------------------------------------------------+
代码 03
好吧,在代码 03 中,我们遇到了一些更复杂的事情。然而,增加的复杂性并不是由于第 30 行。这一行仍然很简单。
代码 03 的复杂性源于我们在第 6 行声明了一个纯动态数组。这就是事情开始变得真正复杂的地方。尽管如此,代码 03 仍然可以被遵循这些文章中介绍的材料、一步一个脚印并练习一路上所演示内容的初学者完全理解。
但是我们看一下运行代码 03 的结果。如下图所示。
图 03
我再次强调图片中的一些信息。充分理解这些突出显示的信息非常重要。
那么,让我们来分解一下这里发生的事情。在第 8 行,我们指定数组将有 7 个元素。然后,在第 11 行到第 15 行,我们初始化其中一些元素。您可以看到在第 18 行,数组的内容包含一些 0 值。这是正常的,因为在第九行,我们正在清除分配数组的内存。
正是由于这些 0,才需要在第 30 行添加一个额外的三元运算符。如果没有它,函数结果将为 0。然而,这并不是关注的重点。真正的兴趣点在第 32 行。请注意,在这里,我们为特定元素赋值,在这种情况下,该元素是数组中的最后一个元素。
但我为什么要这样做,为什么要进行这样的一系列演示?原因是在 MQL5 中有一种使用数组的方法,它允许我们做一些自然不可能的事情。亲爱的读者,除非你理解与数组使用相关的某些概念。与此同时,你会开始注意到这里发生的某些事情,当我们很快探索其他概念时,这些事情会让它们感觉更自然。
好的,我们已经为我们的系统增加了相当程度的复杂性。然而,我们仍然可以让事情变得更加有趣。老实说,如果由我决定,这篇文章将在这里结束,让你练习已经展示过的内容。而且,说实话,这已经很多了,其中许多概念一开始很难理解。但是,让我们做最后一次努力,将一些有趣的东西可视化,这些东西与本文所涵盖的内容直接相关。由于这是一个更复杂的概念,让我们继续下一部分。
远程初始化
在这一点上,亲爱的、受人尊敬的读者和朋友,我问你:停下来研究一下到目前为止所涵盖的内容。一旦你完全理解了一切,在脑海中没有留下任何疑问或困惑,并确保你理解了前面的代码,只有这样,你才能继续本节。如果没有适当的准备,这部分会变得太复杂而无法处理。
到目前为止,我们所看到的表明我们可以使用数组发送数据。之后,我们了解到我们可以在另一个函数或过程中修改数组中的数据。这必须非常小心,尤其是在创建更复杂的应用程序时。接下来,我们看到可以通过调整修改数组值的位置来处理数组。现在是时候做点别的了。这在下一个代码中显示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Infos[]; 07. 08. InitArray(Infos); 09. 10. Print("Based on the values:"); 11. ArrayPrint(Infos); 12. PrintFormat("There are %d possibilities.\nValue after call:", Possibilities(Infos)); 13. ArrayPrint(Infos); 14. 15. ArrayFree(Infos); 16. } 17. //+------------------------------------------------------------------+ 18. void InitArray(uchar &arg[]) 19. { 20. ArrayResize(arg, 7); 21. ZeroMemory(arg); 22. 23. arg[0] = 2; 24. arg[1] = 3; 25. arg[2] = 3; 26. arg[3] = 5; 27. arg[4] = 8; 28. } 29. //+------------------------------------------------------------------+ 30. ushort Possibilities(uchar &arg[]) 31. { 32. ushort info = 0; 33. 34. for (uchar c = 0; c < arg.Size(); c++) 35. info = (c ? (arg[c] ? info * arg[c] : info) : arg[c]); 36. 37. arg[arg.Size() - 1] = 9; 38. 39. return info; 40. } 41. //+------------------------------------------------------------------+
代码 04
亲爱的读者,我们到了这里。这是我们将在本文中介绍的内容的高潮。但是,不要被代码 04 的明显简单性所愚弄。尽管它的输出是我们在下面看到的,但它比看起来要复杂得多。
图 04
请注意,输出与我们在图 03 中看到的完全相同,各个方面都相同。但在这里,在代码 04 中,我以一种简单的方式展示了一些东西,如果处理不当,可能会变得极其复杂。然而,如果以适当的方式正确实施,这种方法可以解决一系列问题。特别是一些所谓的“经验丰富的程序员”声称在 MQL5 中无法完成的事情。至少以纯粹的形式,不依赖于 MQL5 本身提供的任何资源。
我逐渐做出了这些修改,正是为了让你能够了解这里发生的事情。尽管您可能认为代码 04 只是代码 03 所做操作的简单变体,但从本质上讲,代码 04 表明我们确实可以做一些如果不彻底理解某些概念就不可能做到的事情。
其中一个概念 —— 这正是我们在代码04中探索的 —— 是每个数组都是通过引用传递的。当我们这样做时,应用前几篇文章中涵盖的概念,我们确实可以实现许多人声称不可能实现的事情。即:在声明数组的范围之外初始化甚至修改数组。
我们可以进一步推进这一想法,而不会增加我们迄今为止所涵盖的难度。为了解释这一点,让我们巧妙地修改代码 04。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Infos[]; 07. 08. if (ArraySize(Infos)) 09. { 10. Print("Based on the values:"); 11. ArrayPrint(Infos); 12. }else 13. Print("Array has not been initialized yet."); 14. PrintFormat("There are %d possibilities.\nValue after call:", Possibilities(Infos)); 15. ArrayPrint(Infos); 16. 17. ArrayFree(Infos); 18. } 19. //+------------------------------------------------------------------+ 20. void InitArray(uchar &arg[]) 21. { 22. const char init [] = {2, 3, 3, 5, 8}; 23. 24. ArrayCopy(arg, init); 25. } 26. //+------------------------------------------------------------------+ 27. ushort Possibilities(uchar &arg[]) 28. { 29. ushort info = 0; 30. 31. InitArray(arg); 32. 33. for (uchar c = 0; c < arg.Size(); c++) 34. info = (c ? (arg[c] ? info * arg[c] : info) : arg[c]); 35. 36. ArrayResize(arg, arg.Size() + 1); 37. arg[arg.Size() - 1] = 9; 38. 39. return info; 40. } 41. //+------------------------------------------------------------------+
代码 05
当你运行上面的代码 05 时,你将看到下图所示的内容。
图 05
那么,现在你可能会问自己:这怎么可能?这种事根本不合理。但是,亲爱的读者,在这里,我只是对我们从本文开始就在研究的同一段代码有点乐趣。绝对没有理由恐慌或绝望。
关键的细节是,与大多数人通常所做的不同,我们正在突破 MQL5 中完全可探索的概念的极限。然而,在最后几个代码示例中,我们所做的事情有点超出了许多人的理解范围。这是因为他们没有真正理解实现背后的概念。他们只是复制和粘贴代码片段,而没有真正理解代码的工作原理。而这并不是我想要的。亲爱的读者,我希望你能真正理解所涉及的概念。如果你能做到这一点,那么写这些文章并展示这些东西是完全值得的。
好吧,由于我还不想深入探讨其他细节,特别是在本文中,让我们用剩下的部分来详细解释代码 05。毕竟,代码 04 比代码 05 要简单一些。
那么,让我们看看代码 05 是如何工作的。首先,在第 6 行,我们声明了一个纯动态数组。这需要在某个时候分配,这样我们就可以将内存用作存储空间。
假设我们不确定第 6 行的数组是否已初始化。我们使用第 8 行检查数组中是否有任何元素。请记住:它是一个纯动态数组。如果你在第 6 行直接初始化它,那么第 8 行就会获取它。即使在第 6 行和第 8 行之间初始化,它仍然会被第 8 行检测到。
但是,如果初始化恰好在声明数组的第 6 行完成,那么您将遇到其他问题。您可以亲自尝试一下,看看会出现什么样的问题。它将帮助您练习并积累有关数组的经验。
无论如何,如果数组中至少有一个元素,则第 10 行和第 11 行将执行,表明数组中存在元素。如果它尚未初始化,则第 13 行上看到的消息将显示在终端中。
现在,让事情变得真正有趣的部分来了。这是由于第 14 行调用了第 27 行的函数。请注意,到目前为止,数组仍然尚未初始化。只有当第 31 行执行时才会初始化。这是因为第 20 行的程序正是在这一行被调用的。
此过程将在第 24 行初始化在第 6 行创建的动态数组。现在,请注意,要做到这一点,我们将使用静态 ROM 类型数组,如第 22 行所示。我知道这可能看起来很混乱,很难理解。但是如果您看一下标准库中的 ArrayCopy 函数,您就会明白它为什么有效。基本上,此函数的作用是将一个数组复制到另一个数组中。在无数场景中,这在实际应用中非常有用。
之后,我们进入第 33 行的循环,在此执行计算以产生第 14 行的返回值。
然而,在返回到第 14 行之前,我们使用第 36 行向第 6 行声明的数组添加一个新元素,但该元素已在第 24 行初始化。这就是有趣的部分。因为如果您由于从代码中删除第 31 行而没有初始化数组,那么这个 “Possibilities” 函数将返回一个等于第 29 行的值。这是因为第 33 行的循环不会执行。
然而,当你检查第 6 行声明的数组的内容时,你会发现数组中有一个元素。此元素具有第 37 行指定的值。
事实上,你需要在实践中尝试一下,才能完全理解这个解释。因此,正如我之前提到的,练习和使用附件中的内容,以更深入地了解一切是如何运作的。
最后的探讨
在这篇文章中,事情比前几篇文章更令人兴奋,因为我们开始使用从第一篇文章开始就解释过的概念来探索新的可能性。我知道,对于那些没有阅读过之前文章就跳到这里的人来说,所展示的大部分内容可能看起来非常复杂和令人困惑。然而,对于那些一直在练习和学习每篇文章的人来说,你已经知道这些内容有多有趣和有价值。它也为许多可能性打开了大门,其中包括一篇旧文章中简要提及的可能性。
也就是说,在下一篇文章中,我们将把以前见过的东西在你的日常编程中变得如此普遍,以至于你甚至不会注意到你正在成为一个比其他人更好的程序员。通过掌握正确的概念并知道如何应用它们,你将摆脱许多人因为不理解底层编程概念而陷入的局限。他们只是复制并粘贴代码。因此,请尽情享受附件中的文件,我们将在下一篇文章中再见。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15473
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



