
从基础到中级:Include 指令
概述
此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
在上一篇文章 “从基础到中级:BREAK 和 CONTINUE 语句” 中,我们主要关注如何控制利用 WHILE 和 DO WHILE 语句的循环内的执行流程。然而,尽管我想,亲爱的读者,你们可能已经准备好理解 FOR 语句的循环,但我会在讨论循环时稍作停顿。这种中断是必要的,因为在我们继续使用更多的流量控制运算符之前,回顾其他概念是有益的。
因此,在本文中,我们将讨论一个对您非常有帮助的主题。一旦解释清楚,我将能够开始展示更复杂的代码示例。到目前为止,在不利用 MQL5 中可用的某些资源的情况下编写代码对我来说是相当具有挑战性的。尽管你们中的一些人可能认为这对我来说很容易,但事实上,在没有这些资源的情况下编写代码是一个重大的挑战。但现在,我们将在已经可以做的事情列表中添加一些新功能。
我所指的资源是编译指令的使用。如果没有这些指令,我们能做的很多事情都会变得非常有限,迫使我们编写比实际编程场景中所需更多的代码。
与许多人的想法相反,编译指令不会使代码更加混乱。它们的主要目的恰恰相反:使代码更简单、更快、更容易处理和修改。问题是,许多初学者要么忽略了,要么没有学会如何正确使用这个资源。这可能是因为一些编程语言的工具包中没有这样的资源。一些不包含编译指令的语言示例包括 JavaScript 和 Python。虽然这些语言在普通程序员中很受欢迎,但它们不适合创建某些类型的应用程序。然而,这里的目的不是讨论这些语言,而是专注于MQL5。那么,让我们从本文的第一个主题开始。
为什么使用编译指令?
尽管 MQL5 中的编译指令非常适合大多数情况,但有时我会觉得缺少其他指令。这是因为 MQL5 本质上源于 C/C++ 的高度改进。然而,在 C/C++ 中有些指令在 MQL5 中不可用。其中一个指令是 #if,虽然看似不起眼,但在控制我们正在开发的版本的特定部分方面非常有帮助。
话虽如此,即使 MQL5 中没有这个指令(至少在我写这篇文章的时候没有),这里也不会错过。我只是提到这个事实,亲爱的读者,你可能对未来学习 C/C++ 感兴趣,了解 C/C++ 与 MQL5 的一些区别。尽管这里介绍的所有内容(如果不是大部分内容)也可以作为学习 C/C++ 的垫脚石。
简而言之,编译指令有两个主要目的。第一个目的是指导实现更高效的代码模型。第二个目的是允许您创建同一代码的不同版本,而不会实际删除或丢失前面的部分。
我明白,对你们中的许多人来说,这些想法可能看起来很奇怪。这是因为初级程序员经常有删除部分代码以尝试创建新版本的习惯。他们这样做是为了纠正潜在的问题或改进某些计算或操作的执行方式。
然而,这种方法只需要在不存在编译指令的语言中使用。允许并包含此类资源的语言可以同时容纳同一代码的各种迷你版本。在版本之间进行选择的方法正是通过使用指令,以智能和有组织的方式完成。
作为这个组织的一部分,程序员需要一些经验。此时此刻,我们将从最基础的开始。换句话说,我将假设您,我亲爱的读者,完全不知道如何使用编译指令来处理、处理和实现代码。
然而,随着即将发布的文章的发布,我将逐步向您展示如何整合与编译指令相关的活动。最有可能的是,我不会写一篇只关注这个特定主题的文章。这只是对该主题的介绍,以便您了解什么是编译指令。
现在我们已经介绍了该主题的一般介绍,让我们从 MQL5 代码中最常见的指令开始。但为此,我们将转向一个新的主题。
#INCLUDE 指令
最有可能的是,亲爱的读者,这将是您在代码中最常遇到的编译指令,特别是在 MQL5 和 C/C++ 风格的代码中。这是为什么呢?原因是,即使不是全部,很大一部分经验丰富的程序员也不喜欢将所有内容放入单个代码文件中。通常,随着时间的推移,你会逐渐理解,经验丰富的程序员会将他们的代码分解成更小的块。这些块通常会演变成一个函数、过程、结构和类的库,所有这些都以高度逻辑的方式组织。这种组织使编程,即使在创建新的和独特的代码时,也能以极快的速度完成,只需要最小的修改。目标是将程序员随着时间的推移仔细编目的原始代码转换为新版本的代码。
同时,您可能会发现自己反复键入代码以完成相同的任务。
然而,我不会教您如何组织您的代码来使用这个指令。事实上,没有人会教你如何做到这一点,因为只有当维护代码的人仔细而细致地选择每个元素的放置位置时才有意义。话虽如此,虽然我不会教您如何以这种方式组织代码,但我可以解释如何以如此谨慎和专注的方式访问您创建的代码。这是该指令的主要目的:允许您以非常自然和实用的方式访问内容。
在这种情况下,不涉及执行流程。虽然有一些细节需要观察,但我们将逐步解决这些问题,以便您能够以自然的方式理解它们。
首先,让我们采用前几篇文章中介绍的代码之一。这将使我们在这里讨论的内容更加熟悉。为此,让我们从构建一个小的初始代码开始。您可以在下面看到:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. ulong value; 07. 08. Print("Factorial of 5: ", Factorial(5)); 09. Print("Factorial of 3: ", Factorial(3)); 10. Print(One_Radian()); 11. do 12. { 13. value = Tic_Tac(); 14. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", value); 15. }while (value < 3); 16. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac(true)); 17. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac()); 18. } 19. //+------------------------------------------------------------------+ 20. double One_Radian() 21. { 22. return 180. / M_PI; 23. } 24. //+------------------------------------------------------------------+ 25. ulong Tic_Tac(bool reset = false) 26. { 27. static ulong Tic_Tac = 0; 28. 29. if (reset) 30. Tic_Tac = 0; 31. else 32. Tic_Tac = Tic_Tac + 1; 33. 34. return Tic_Tac; 35. } 36. //+------------------------------------------------------------------+ 37. ulong Factorial(uchar who) 38. { 39. static uchar counter = 0; 40. static ulong value = 1; 41. 42. if (who) who = who - 1; 43. else 44. { 45. counter = 0; 46. value = 1; 47. } 48. while (counter < who) 49. { 50. counter = counter + 1; 51. value = value * counter; 52. }; 53. while (counter > who) 54. { 55. value = value / counter; 56. counter = counter - 1; 57. }; 58. counter = counter + 1; 59. return (value = value * counter); 60. } 61. //+------------------------------------------------------------------+
代码 01
亲爱的读者,您必须完全能够理解此代码 01。这是我们继续前进的先决条件。如果您无法理解此代码,请立即停止并返回前面的文章。该代码非常简单,在任何情况下都不会令人困惑或难以理解。
执行时,此代码将在 MetaTrader 5 终端中产生以下输出:
图 01
我们这样做只是为了验证它是否有效。由于我们可以清楚地看到它按预期运行,我们现在可以开始讨论如何在这里使用编译指令。也许我应该从另一个指令开始,但没关系。由于 #include 指令是所有指令中最常用的,因此从它开始是有意义的。那么,让我们继续吧。
在做其他事情之前,你需要了解的第一件事是如何划分你的代码。这看上去微不足道,但事实并非如此。如果你没有想出一个适合你并且高度可用的方法,随着时间的推移,你在尝试创建新代码时会遇到严重的问题。然而,如果你设计出一种适合你的方法,你会走得很远。
由于这里的目标纯粹是教育性的,我们将把内容分为三个单独的文件。每个都将包含一个最初在代码 01 中看到的函数或过程。
考虑到这一点,您可能会认为:“好的,我会创建文件。”但这不是你应该采取的第二步,亲爱的读者。事实上,在此之前还有一步。你应该采取的第二步是回答:我将在哪里放置我创建的文件?请等一等,目录不应该就是 include 目录吗?这是一个比其他任何问题都更私人的问题。原因是最好的位置并不总是 include 目录。如果您不确定我在说什么,只需通过 MetaEditor 导航到 MQL5 文件夹,如下图所示:
图 02
当您执行此操作时,您将在 MQL5 目录中看到一个名为 “include” 的文件夹。这是我们将要创建的文件类型(通常称为头文件)的默认目录。然而,正如我之前提到的,这并不总是最好的选择。根据项目或您试图实现的目标,将所有头文件放置在 include 目录中可能会导致问题。这是因为同一过程或函数的略有不同的版本可能会与 include 文件夹中可能或应该包含的另一个版本冲突。
然而,许多人可能会问:我们不能创建子目录来更好地组织我们的头文件吗?是的,事实上,这是最常见的做法之一。然而,即使在 include 文件夹中使用子目录的方法,在某些情况下,它仍然不理想。
但在任何人开始恐慌之前,我确实会向你展示如何处理这件事。这是为了帮助您以最佳方式组织自己的代码。正如我之前所说,没有人能教你如何做到这一点。但是,通过了解如何做到这一点,你将能够创建自己的组织结构。
所以,让我们首先做一些完全不同的事情。我们将在 Scripts 文件夹中创建一个子目录。该子目录将包含代码 01 中看到的每个函数。但为了正确地区分这些事情,我们将分部分解决这个问题,从解决方案 1 开始。
解决方案 1
分离代码 01 中所示函数的第一个解决方案是将每个函数放在头文件中。但是,这些文件将位于 Scripts 目录中的文件夹中。一个重要的细节:始终对您创建的头文件使用 .MQH 扩展名。这样,只需查看操作系统的文件资源管理器,就可以更容易地识别文件是什么。话虽如此,我们还是做出了划分。这确保了每个文件都包含以下内容。
1. //+------------------------------------------------------------------+ 2. #property copyright "Daniel Jose" 3. //+------------------------------------------------------------------+ 4. double One_Radian() 5. { 6. return 180. / M_PI; 7. } 8. //+------------------------------------------------------------------+
File 01
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. ulong Tic_Tac(bool reset = false) 05. { 06. static ulong Tic_Tac = 0; 07. 08. if (reset) 09. Tic_Tac = 0; 10. else 11. Tic_Tac = Tic_Tac + 1; 12. 13. return Tic_Tac; 14. } 15. //+------------------------------------------------------------------+
File 02
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. ulong Factorial(uchar who) 05. { 06. static uchar counter = 0; 07. static ulong value = 1; 08. 09. if (who) who = who - 1; 10. else 11. { 12. counter = 0; 13. value = 1; 14. } 15. while (counter < who) 16. { 17. counter = counter + 1; 18. value = value * counter; 19. }; 20. while (counter > who) 21. { 22. value = value / counter; 23. counter = counter - 1; 24. }; 25. counter = counter + 1; 26. return (value = value * counter); 27. } 28. //+------------------------------------------------------------------+
File 03
另一个重点:一旦完成划分,您就可以决定项目的放置位置以及每个文件的名称。这里没有固定的规则。您可以自由做出自己的选择。
现在代码已经从代码 01 中提取出来并放置在单独的文件中,我们最终得到的代码如下:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. ulong value; 07. 08. Print("Factorial of 5: ", Factorial(5)); 09. Print("Factorial of 3: ", Factorial(3)); 10. Print(One_Radian()); 11. do 12. { 13. value = Tic_Tac(); 14. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", value); 15. }while (value < 3); 16. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac(true)); 17. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac()); 18. } 19. //+------------------------------------------------------------------+
代码 02
太好了!它看起来比我们在代码 01 中看到的要简单得多。亲爱的读者,确实如此。但是,如果您尝试编译此代码 02,您将遇到编译器报告的大量错误。如下图所示,这些错误表明编译器无法解释代码。
图 03
实际上,并不是编译器不理解代码。问题在于编译器不知道如何解析代码中出现的过程和函数调用。但这意味着什么?与许多人对编程语言的想法或假设相反,编程语言实际上由两个组件组成。第一个组件被称为标准库。这个标准库定义了我们用来创建所谓的用户级代码的函数、过程、保留字、常量和其他元素。
作为使用任何给定语言的程序员,您无法更改标准库的功能。但是,您可以利用其中定义的内容来构建自己的解决方案。标准库中的所有内容都可以使用,无需任何特殊操作。但是,必须将此库之外的任何内容显式添加到代码中。这使得编译器知道如何解决可能出现的每个函数或过程调用。这就是为什么当您尝试编译代码 02 时,尽管它与代码 01 相似,但它无法编译。
要成功编译代码,您必须明确告诉编译器编译过程中应包含哪些文件。这正是为什么这个指令被称为编译指令,并且它有一个非常合适的名称 #include。换句话说,它告诉编译器:在编译此代码的过程中包含此文件。亲爱的读者,现在你明白了吗?
如果你真正掌握了这个概念,你将能够完成以前无法完成的事情。即使在尝试学习编程时,某些方面似乎也不清楚或缺乏具体意义。但我们将在另一个时间更详细地讨论这个问题。我不想用太多的信息让你不知所措。我希望你能充分理解和吸收这些文章中解释和演示的内容。
现在,如果仅仅因为编译器不知道如何访问必要的信息而无法编译代码 02,我们该如何解决这个问题?我们是否应该手动打开刚刚创建的文件,复制每个函数或过程,并将其直接粘贴到代码 02中,使其再次类似于代码 01?毕竟,如果代码 01 编译成功,则意味着它是正确的。但这种方法似乎并不完全合乎逻辑。我见过其他程序工作,它们不需要将整个代码段复制粘贴到最终脚本中。现在我很好奇。如何解决这个问题?这是比较容易的部分。您只需执行与以下代码所示类似的操作:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include "Tutorial\File 01.mqh" 05. #include "Tutorial\File 02.mqh" 06. #include "Tutorial\File 03.mqh" 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. ulong value; 11. 12. Print("Factorial of 5: ", Factorial(5)); 13. Print("Factorial of 3: ", Factorial(3)); 14. Print(One_Radian()); 15. do 16. { 17. value = Tic_Tac(); 18. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", value); 19. }while (value < 3); 20. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac(true)); 21. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac()); 22. } 23. //+------------------------------------------------------------------+
代码 03
现在到了有趣的部分。当你尝试编译代码 03 时,你会得到一个类似于下面所示的响应:
图 04
换句话说,成功。但怎么做呢?原因在于代码 03 的第四、五、六行。从技术上讲,这些行可以放置在代码中的任何位置。然而,出于组织目的,它们通常放在脚本的开头。在极少数情况下,它们可能会出现在其他地方,但这些都是不适用于这里的具体情况。无论如何,能够以一种更有组织和实用的方式构建事物是件很棒的事情。
然而,有一个重要的概念,你至少需要了解基础知识。我们将在以后的文章中更深入地探讨这一点。关键在于如何在代码03中声明每个 #include 指令。
目前,我不会对此进行详细解释。相反,我鼓励你阅读文档。如果我现在解释它,你可能很难理解为什么在代码 03 中是这样做的。更糟糕的是,你最终可能会更加困惑,而不是清楚为什么声明在某些情况下以一种方式出现,而在其他情况下则以不同的方式出现。
在文档中,您可以在 “包含文件(#include)” 下找到更多详细信息。然而,那些关注我一段时间的人已经知道,代码 03 中第四、五、六行的写法是有特定原因的。
无论如何,当您执行代码 03 时,您将获得与图像 01 中所示的相同输出。现在,让我们继续讨论第二种解决方案。为了清楚地将其与这个部分分开,让我们引入一个新的部分。
解决方案 2
第二种解决方案遵循可用性原则。换句话说,我们创建的东西扩展了 MQL5 创建新代码或更快生成代码的能力。看到其他程序员分发最初包含在 MetaTrader 5 中的修改后的头文件并不罕见。就我个人而言,我发现这些发行版非常有用,因为一些修改可能非常有趣。然而,问题是把这些文件放在哪里。
这很重要的原因是 MetaTrader 5 会定期更新。如果您有一个修改过的头文件,无论是由其他人创建还是由您自定义,并且您发现它对您的开发工作非常实用和有用,则不应该将其存储在任何地方。如果它位于 MQL5 文件夹中的 include 目录内,则下一次 MetaTrader 5 更新可能会覆盖它。在这种情况下,您将丢失宝贵的文件。
有一个解决方案:重命名修改后的头文件。但是,您也可以使用上一节中描述的方法。两种方法都可以。但是,在第一种方法中,您将面临一些限制。例如,访问存储在当前目录之外的头文件将更加困难。这并非不可能,但确实需要额外的管理步骤。
因此,当我们想在多个不相关的应用程序中使用相同的头文件时,最好将其存储在一个中心位置。在这里,我们在 include 目录中执行此操作。但是,请始终记住创建定期备份。理想情况下,您应该使用版本控制系统来有效地管理您的文件。
为此,我建议使用 GIT。您可以在我的另一篇文章中了解更多信息:“GIT:它是什么?” 。正确使用 GIT 将使您免于无数的头痛和不眠之夜。但是,当然,你需要学习并学会正确使用这个工具。
现在回到我们的主题。我们现在可以重用上一节中的相同文件,但将它们存储在 “include” 目录中。这允许我们维护同一文件的两个不同版本:一个是您创建的任何应用程序都可以轻松访问的版本,另一个是我们在这里工作的项目特有的版本。为了证明这一点并证明这是可能的,让我们修改上一节中的一个文件。事实上,我们现在将维护它的两个版本:一个仅可用于当前项目,另一个可用于您开发的任何脚本。该文件如下所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. double One_Radian() 05. { 06. return 180. / M_PI; 07. } 08. //+------------------------------------------------------------------+ 09. double ToRadians(double angle) 10. { 11. return angle / One_Radian(); 12. } 13. //+------------------------------------------------------------------+ 14. double ToDegrees(double angle) 15. { 16. return angle * One_Radian(); 17. } 18. //+------------------------------------------------------------------+
File 04
由于我们从最基础的开始,我不会展示一些我们可以做的事情。我将重点介绍如何使用头文件。现在我们有两个文件,其中包含同一函数的两个相同版本,在本例中为 One_Radians 函数。虽然现在这对你来说似乎微不足道,也不重要,但随着我们深入挖掘并揭示新功能,我们会发现这种情况是有用的。我们会在适当的时候解决这个问题。
现在我们希望使用 File 04 而不是 File 01。这是因为它包含我们想要使用的其他函数。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include "Tutorial\File 01.mqh" 05. #include "Tutorial\File 02.mqh" 06. #include "Tutorial\File 03.mqh" 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. ulong value; 11. 12. Print("Factorial of 5: ", Factorial(5)); 13. Print("Factorial of 3: ", Factorial(3)); 14. Print(One_Radian()); 15. Print(ToRadians(90)); 16. do 17. { 18. value = Tic_Tac(); 19. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", value); 20. }while (value < 3); 21. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac(true)); 22. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac()); 23. } 24. //+------------------------------------------------------------------+
代码 04
如果您尝试编译代码 04,您将得到下图所示的结果。
图 05
这证明代码中存在错误。然而,解决这个问题很简单,尽管作为一名程序员需要仔细注意。当处理包含同名函数或过程的不同版本的头文件时,会出现挑战。这就是事情变得棘手的地方。你需要学会如何自己处理这种情况。不幸的是,没有通用的方法或简单的解释来处理这个问题。原因很简单:这一切都取决于你如何随着时间的推移构建代码。除了这种复杂性之外,为了这个教育示例的目的,解决方案非常简单。事实上,错误消息是由于第 15 行缺少一个函数造成的。由于此函数不在代码 04 中包含的任何头文件中,编译将始终失败。为了解决这个问题,我们需要告诉编译器在哪里找到正确的文件,该文件包含第 15 行工作所需的函数。解决方案是将代码更新为代码 05,如下所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include <Tutorial\File 01.mqh> 05. #include "Tutorial\File 02.mqh" 06. #include "Tutorial\File 03.mqh" 07. //+------------------------------------------------------------------+ 08. void OnStart(void) 09. { 10. ulong value; 11. 12. Print("Factorial of 5: ", Factorial(5)); 13. Print("Factorial of 3: ", Factorial(3)); 14. Print(One_Radian()); 15. Print(ToRadians(90)); 16. do 17. { 18. value = Tic_Tac(); 19. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", value); 20. }while (value < 3); 21. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac(true)); 22. Print(__FUNCTION__, " ", __LINE__, " Tic Tac: ", Tic_Tac()); 23. } 24. //+------------------------------------------------------------------+
代码 05
请注意,变化似乎很小。然而,这个小小的调整是有意的。它有助于证明练习和真正理解现实世界编码中的工作原理是至关重要的。一旦您尝试编译代码 05,结果将如下所示:
图 06
这证实了第 4 行中包含的文件实际上位于 include 目录中。您可以在附件中看到这一点,该文件直观地演示了如何在实践中组织所有内容。本文将展示的最后一件事是执行代码 05 的结果:
图 07
最后的探讨
在本文中,我们探讨了最常用的编译指令之一。虽然我们只关注一个方面,但不可能在一篇文章中涵盖该指令可以做的所有事情。即使我们单独创建多篇专门针对该指令的文章,某些方面仍然难以充分解释。这是因为真正让一个人成为伟大程序员的不仅仅是构建程序以产生结果的能力。这是关于组织自己作为开发人员的身份。这意味着在头文件中创建和编目有用和常用的代码片段,以便在日常编程任务中更容易重用。
学习如何做到这一点?这不是任何人都可以教你的东西。这只是你通过练习和时间才能掌握的东西。但是,你必须迈出第一步。本文的目标正是引导您完成第一步。亲爱的读者,我希望你觉得这篇文章有用。在下一篇文章中,我们将探讨另一个控制流语句。那么,到时候见!
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15383


