从基础到中级:重载
概述
在上一篇名为 “从基础到中级:浮点“ 的文章中,我们讨论了浮点数运算的基础知识。我首先把这篇材料作为一个起点,因为它非常重要。在我看来,为了进一步应对更复杂的问题,了解 float 和 double 类型的工作原理是绝对必要的。
尽管该篇文章的内容仅涵盖了每个人想要成为一名优秀的程序员真正需要掌握的基本部分,但这已经足以让我们继续讨论其他主题。然而,在某些时候,我们很可能不得不回到浮点数,但要达到更高级的水平。然而,凡事都有其时机。
尽管如此,在我看来,这篇文章已经帮助许多人理解,当我们处理一个程序,或者更确切地说,处理一个交易平台时,错误意味着亏损,我们不能在计算中追求绝对的准确性。这是因为浮点数的本质不允许我们绝对准确和精确。我们所能做的就是尽量接近我们认为可以接受或合理的价值,以便我们能够对买入或卖出特定资产或金融工具做出决定。
然而,这些问题更多地与平台的运营商或用户有关,而不是与我们的主要活动有关,我们的主要活动是编程。作为程序员,我们必须提醒应用程序的用户,计算中存在一定的风险。此外,用户或交易者自己必须独立调整数据,并在做出买入或卖出决定时分析这些数据对他有多大意义。
然而,我仍然没有看到我们拥有足够坚实和完善的基础,以便我们可以开始讨论如何编程或实现指标或 EA 交易(也称为机器人)。虽然我们已经很清楚什么是必要的,什么是可以做的,但在我看来,在某些时候,我们可能仍然会发现自己束手无策。这是因为有些事情还没有得到解释。在不同的情况下,出于不同的目的,它们确实是必要的。它们经常允许我们做那些没有适当知识就无法实现的事情。
由于我对在未来的文章中深入探讨过于乏味和繁琐的细节感到不满,我想为展示更先进的材料创造一个完整而真正坚实的基础。这不仅会使文章更有趣,还有助于提高所提供内容的质量。毕竟,在不浪费时间解释这些细节的情况下,我将能够在未来的材料中专注于传授更多的知识。
还有一些事情需要解释,这对许多初学者来说可能相当困难。在我们进入更高的层次之前,确实有必要花时间研究它们。
正如对数组和字符串所做的那样,我在这里展示了一个而引出另一个,在继续处理更复杂的问题之前,这里还需要澄清一种机制。原因很简单:一个看似令人困惑的机制如何允许实现另一个机制,这将变得更加清楚,许多初学者之所以不使用,仅仅是因为他们不理解它。他们被迫创建许多在更复杂的代码中通常完全不必要的例程、函数和过程。或者更确切地说,是由对如何用特定语言实现某些东西有更深入了解的程序员编写的代码。在我们的例子中 — 使用 MQL5。
因此,为了将两者分开,让我们继续讨论一个新主题。
什么是重载?
如果有什么东西可以完全混淆新手程序员甚至业余程序员,使他们无法理解代码片段中的至少一些东西,那就是同一代码中有两个同名函数或过程。是的,这种情况确实会发生,当它发生时,经验不足或不太精明的程序员会完全迷失方向,无法修复代码、改进代码,甚至无法基于它创建自己的代码。
尽管对于不知情的人来说,这类事情可能看起来非常混乱,完全没有逻辑,但只要遵循某些简单的规则,编译器就可以接受。
我理解并知道,很多人可能会说我失去了理智或完全失去了叙事的线索,因为他们根本不知道如何使用两个目的不同但同时包含相同名称的调用。让我们举一个简单的例子 —— 真的非常简单 —— 只是为了说明我在说什么以及我将要解释什么。
现在,让我们看看下面的代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(-10, 25.)); 08. } 09. //+------------------------------------------------------------------+ 10. ulong Sum(ulong arg1, ulong arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+ 16. double Sum(double arg1, double arg2) 17. { 18. Print(__FUNCTION__, "::", __LINE__); 19. return arg1 + arg2; 20. } 21. //+------------------------------------------------------------------+
代码 01
所以,亲爱的读者,我希望你在看到结果之前,尽可能诚实地看待代码 01:这段代码到底会如何运行?或者,更清楚地说,您应该已经知道,当我们在代码中使用 Print 调用时,我们希望在 MetaTrader 5 终端上输出一些值。这里我们有四次 Print 库过程的调用。
因此,重新表述上面提出的问题:你能告诉我终端上将显示什么吗?显然,您可以查看第 6 行和第 7 行并说:“好吧,将打印值 35 和值 15”。这是操作中最明显的部分。我感兴趣的是将执行这两个函数中的哪一个:第 10 行的 Sum 函数还是第 16 行的 Sum 函数?嗯,让我想想。哎呀,但是它不会编译,因为我们有两个同名的函数 —— 在第 10 行和第 16 行。你想抓住我这个问题吗?
好吧,我亲爱的读者,实际上,你完全正确,这里确实可能有一个陷阱,因为两个函数或过程不能具有相同的名称。这是事实,任何声称不是这样的人都是在撒谎,或者至少在隐瞒什么。然而,这并不适用于这种情况,无论听起来多么奇怪。第 10 行和第 16 行所示的两个 Sum 函数不是同一个函数。尽管它们都做同样的工作并且正确完成,但编译器并不将它们视为一个函数,而是视为两个不同的函数,即使它们具有相同的名称。
但是,现在我真的很困惑,因为据我所知,当我们要在同一个代码中使用两个函数或过程时,它们不能具有相同的名称。事实确实如此 —— 它们不能。但在这里,我们所做的就是所谓的函数或过程的重载。
重载问题是编程时给我带来最大乐趣的事情之一。因为如果正确规划重载,它可以让我们实现更具可读性的代码。尽管有更有效的方法来实现同样的事情,但如果不了解代码 01 中看到的重载是什么,那么理解 MQL5 语言中存在的另一种机制几乎是不可能的。但让我们稍后再说。首先,让我们弄清楚什么是重载以及它是如何工作的。
重载正是您在代码 01 的第 10 行和第 16 行中看到的。完成后,将生成下图所示的内容。

图 01
我们感兴趣的不是将在第 6 行和第 7 行打印的值,而是图中突出显示的值。这是我们目前真正感兴趣的数据。请注意,在一种情况下我们指的是第 12 行,而在另一种情况下我们指的是第 18 行。为什么这样?
原因在于第 6 行和第 7 行的调用中传递的值的类型。当编译器尝试生成可执行文件时,它将查看这些值并说:好的,这个值对应于这个函数的预期参数;那个值对应于另一个参数。因此它将允许以这样的方式进行调用,即我们将拥有多个具有相同名称的调用。
然而,这一点很重要,参数或预期参数可能不一样。它们可能有一些相似之处,但数量必须不同,或者至少其中一个必须是不同类型的。否则,编译器将无法理解代码应该指向哪里,这将被视为错误。
请注意,在这种情况下,第 10 行的 Sum 和第 16 行的 Sum 使用不同的类型。由于第 7 行中的第二个参数在类型上与整数不同,因此编译器理解在这种情况下需要调用一个需要浮点参数的函数。
为了巩固这些知识,让我们看另一个例子。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum((char)-10, 25)); 08. } 09. //+------------------------------------------------------------------+ 10. ulong Sum(int arg1, ulong arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+ 16. double Sum(char arg1, ulong arg2) 17. { 18. Print(__FUNCTION__, "::", __LINE__); 19. return (double)(arg1 + arg2); 20. } 21. //+------------------------------------------------------------------+
代码 02
现在要小心,因为代码 02 给出的结果与图 01 所示相同。然而,我们这里要处理的事情并不像以前那么简单。这是许多经验不足的程序员觉得太混乱的代码类型,所以他们试图更改它以消除由此产生的混乱。然而,这样做最终会给自己带来问题,因为结果与预期或代码作者的意图不符。
因此,亲爱的读者,在尝试更改未知代码之前,请先研究一下它是如何工作的。如果可能的话,在受控环境中运行它,以便您了解进度。您可能会错过一些东西,试图在没有意识到的情况下改变一些东西,这是初学者犯的最大错误之一。
现在让我们来弄清楚:代码 02 第 10 行和第 16 行所示的 Sum 函数仍然返回相同类型的值。请注意,我们在第 19 行使用了显式转换,以便编译器不会抱怨类型不匹配。
但现在以下一点很重要:请注意第 10 行的 Sum 函数和第 16 行的 Sum 之间的区别在于它们采用的第一个参数的类型。通常,这会让很多人感到困惑,因为乍一看,从第 10 行开始对函数使用 4 字节类型,从第 16 行开始对函数使用 1 字节类型似乎毫无意义。
事实上,在这个例子中,这是完全不必要的。然而,由于某种原因,程序员可能希望以这样的方式在函数内部工作,即第 16 行的函数实际上应该采用 char 类型的值,因为 int 类型可能不适合。让我提醒你,我们可以用另一种方法解决这个问题,但这将在另一篇文章中讨论。这里我们假设你不知道如何做到这一点,所以你决定按照代码 02 所示来实现它。
问题:这样不好吗?不,我亲爱的读者,创建、编写和实现如代码 02 所示的代码并不是一件坏事。这只是意味着你应该更多地了解如何在 MQL5 中编程。但毫无疑问,这并不坏。
所以,这些都是简单的重载形式。还有另一种形式,我们不是声明不同类型的参数,而是使用不同数量的参数。这种情况可能是最常见的,因为与 C 和 C++ 不同,在那里我们可以在不改变函数或过程声明的情况下实现几乎无限数量的参数,但在 MQL5 中我们无法做到这一点。好吧,这是在测试模式下,因为有一种方法可以实现它。我在上一篇文章中展示了如何操作,尽管这只是对解释某些机制时实际可以做什么的介绍。
但让我们再次考虑到,亲爱的读者,你真的在学习,想好好学习。那么,这种其他形式的重载是什么样的呢?您可以在下面看到它。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(Sum(10, 25)); 07. Print(Sum(-10, 25, 4)); 08. } 09. //+------------------------------------------------------------------+ 10. long Sum(long arg1, long arg2) 11. { 12. Print(__FUNCTION__, "::", __LINE__); 13. return arg1 + arg2; 14. } 15. //+------------------------------------------------------------------+ 16. long Sum(long arg1, long arg2, long arg3) 17. { 18. Print(__FUNCTION__, "::", __LINE__); 19. return arg1 + arg2 + arg3; 20. } 21. //+------------------------------------------------------------------+
代码 03
在这个例子中,结果将如下图所示。

图 02
据我所知,大多数初学者发现这种类型的代码比其他代码更容易理解,如示例 03 所示,因为它更容易理解在每种特定情况下将调用哪个函数或过程。许多人并不认为这种实现(在代码 03 中所呈现的)是一种重载形式。然而,在文献中,许多作者确实认为这是一种重载,因为函数名称保持不变。
好的,我想现在已经清楚什么是重载,什么不是重载了。然而,仍然存在一个问题:为什么这有效,编译器如何仅根据使用的不同类型来区分这两种实现?至少乍一看,这似乎不太合逻辑。
实际上,亲爱的读者,这并不完全符合逻辑,因为当我们使用一个函数时,我们使用声明它的名称来调用它,而不是使用其他名称。那么它为什么有效呢?原因是,为了使代码真正成为可执行的,编译器应该了解如何正确地引导执行流。当使用重载时,在函数和过程的情况下,编译器会创建一个唯一的内部名称。我说“内部”是因为你不知道它将如何形成,尽管一般来说,它是使用一些简单的规则完成的。
例如:查看代码 03,在第 10 行,编译器可以将这个 Sum 函数调用为 Sum_long_long ;而它可以从第 16 行调用该函数 Sum_long_long_long 。请注意,尽管看起来很简单,但这已经产生了很大的不同,因为它与创建具有唯一名称的函数非常相似。
代码 02 也同样如此。在那里,编译器可以将第 10 行的函数解释为 Sum_int_ulong ,将第 16 行的函数解释为 Sum_char_ulong 。请注意,现在一切都开始变得合理了,由此可见,编译器能够理解在任何给定时刻执行流程应该指向哪里。
重要事项:我在这里使用这个术语仅用于教学目的,因为根据实现和编译器开发人员的意图,这个术语可能完全不同。请记住,编译器内部会执行类似的事情。
太好了,我们几乎已经完成了使用重载的话题。只缺少两个例子,在我看来,展示这两个例子非常有趣。第一个如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int v = -5; 07. 08. Sum(v, Sum(10, 25)); 09. Print(v); 10. } 11. //+------------------------------------------------------------------+ 12. long Sum(long arg1, long arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. return arg1 + arg2; 16. } 17. //+------------------------------------------------------------------+ 18. void Sum(int &arg1, long arg2) 19. { 20. Print(__FUNCTION__, "::", __LINE__); 21. 22. arg1 += (int)arg2; 23. } 24. //+------------------------------------------------------------------+
代码 04
在这个例子中,我们可以选择将重载函数与过程一起使用。请注意,它们具有相同的名称,但由于类型的不同,您可以将执行流重定向到一个方向或另一个方向。值得注意的是,根据此 04 代码的实现方式、所使用的参数类型以及代码本身的结构,它可能会产生一个或另一个结果。这一切都是因为我们在声明中改变了一些小东西:无论是类型还是第 8 行的解释方式。
请注意以下事实:尽管第 8 行似乎引用了第 12 行提供的函数,但使用变量作为第一个参数这一事实清楚地表明我们打算调用第 18 行。但是,如果第 8 行声明的类型与第 18 行预期的类型不兼容,那么我们实际上永远无法调用第 18 行,因为所有操作都将在第 12 行执行。因此,不建议不当或不谨慎地使用函数和过程的重载,因为这可能会导致与预期大相径庭的结果。
然而,以代码 04 的形式呈现,当执行时,我们将看到下图所示的结果。

图 03
同样,值得注意的是,出现这个结果完全是因为类型完全匹配,编译器能够正确理解我们的意图。但是,如果某个程序员在没有研究和理解代码的情况下,决定不想在代码中使用 long 类型(因为它是 64 位)而决定只使用 int 类型(因为它是 32 位)—— 看看会发生什么。进行所述更改后,代码 04 将变成代码 05,如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. int v = -5; 07. 08. Sum(v, Sum(10, 25)); 09. Print(v); 10. } 11. //+------------------------------------------------------------------+ 12. int Sum(int arg1, int arg2) 13. { 14. Print(__FUNCTION__, "::", __LINE__); 15. return arg1 + arg2; 16. } 17. //+------------------------------------------------------------------+ 18. void Sum(int &arg1, int arg2) 19. { 20. Print(__FUNCTION__, "::", __LINE__); 21. 22. arg1 += (int)arg2; 23. } 24. //+------------------------------------------------------------------+
代码 05
请注意,这是一个完全无害的更改,并没有造成问题的特别意图,但在尝试创建可执行文件时,编译器并不了解具体实现了什么。编译器输出中打印的消息如下所示。

图 04
也就是说,在程序员看来,将代码更改为更合适的代码的完全无害的尝试最终导致代码变得完全无法理解,因为编译器不知道在执行第 8 行时是寻址到第 12 行还是第 18 行。
然而,我亲爱的读者,我希望你在这里明白一件事:错误不在第 8 行。错误实际上在于,在编译器的理解中,我们有两个完全相同的调用。经验不足的程序员很快就会认为问题出在第 8 行,尽管事实上问题出在第 12 行或第 18 行,如上所述,因为编译器可以创建一个唯一的内部名称来尝试生成最终的可执行文件。
这种方法在这里看似简单,但在实践中却极其困难。因为其中一个调用可能在一个头文件中,另一个可能在与第一个完全无关的完全不同的头文件中。这使得情况变得更加复杂和混乱。
作为最后一个例子,让我们考虑重载的另一个用例。如下面代码所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. long v = -16, 07. m[]; 08. bool b; 09. 10. Add(m, Sum(10, 25)); 11. Print("Return: ", b = Sum(v, m)); 12. if (b) Print("Value: ", v); 13. ArrayFree(m); 14. } 15. //+------------------------------------------------------------------+ 16. void Add(long &arg[], long value) 17. { 18. ArrayResize(arg, arg.Size() + 1); 19. arg[arg.Size() - 1] = value; 20. } 21. //+------------------------------------------------------------------+ 22. long Sum(long arg1, long arg2) 23. { 24. Print(__FUNCTION__, "::", __LINE__); 25. return arg1 + arg2; 26. } 27. //+------------------------------------------------------------------+ 28. bool Sum(long &arg1, const long &arg2[]) 29. { 30. Print(__FUNCTION__, "::", __LINE__); 31. 32. if (arg2.Size() == 0) 33. return false; 34. 35. for (uchar c = 0; c < arg2.Size(); c++) 36. arg1 += arg2[c]; 37. 38. return true; 39. } 40. //+------------------------------------------------------------------+
代码 06
这个案例确实非常有趣,并且具有许多实际意义。因为通常,至少在 MQL5 环境中,目标是创建价格和交易的图形解释机制,我们实现的代码的主要任务或目的是使用多个报价执行一些分解。通常,这种分解在某种程度上与开发用于 EA 交易的特定指标或交易模型有关,目的是形成视觉图形信号,并吸引交易者或 MetaTrader 5 平台用户对开仓或平仓的注意力,或至少对某个可能在某个时间点发生的某个特定走势的注意力。
我知道很多人,尤其是初学者,都渴望创造一些东西,并立即看到它是如何执行的。然而,在我们这样做之前,有必要深入探讨我们目前在这些文章中考虑的事情。在这里,材料更为基础,旨在创建一个坚实的基础,以便您以后可以处理更复杂、更面向任务的事情。由于我不想在未来花太多时间解释简单的细节,我现在正在创建这个知识库。因此,亲爱的读者,你将能够适应甚至改进你在文章中看到的更专业的内容,不仅是我的,还有你想使用的任何其他程序员的想法或代码。
因此,在这个代码 06 中,我们只有一个重载的示例,在一种情况下,我们以一种非常有趣的方式使用离散值,而在另一种情况下,我们使用数据数组中包含的一组值。然而,尽管外部简单,但由于在理解一些基本概念时出现了一些小错误,我们在执行计算时经常遇到问题。这些错误之一是我们经常不注意传递给函数的参数的初始值。这些类型的错误有时很难追踪和修复,至少在我们了解失败的真正原因之前是这样。
但在实践中,这些事情更容易理解,随着经验的积累,你犯的错误会越来越少。如果它们仍然发生,那只是由于在实施过程中缺乏勤勉。
现在让我们看看代码 06 中有什么。执行上面显示的代码时,您将得到以下结果,如下所示。

图 05
现在明白了,亲爱的读者。如前所述,我们在执行多重分解时经常会遇到问题。这是因为我们在分解时使用了错误数量的元素。为了避免这种情况,我们拒绝从函数返回值,而是返回一个特性。它将指示是否接受因子化值。因此,我们在第 32 行检查这一点。请注意,Sum 函数仍然重载,因为在第 22 行中它与在第 28 行中具有相同的名称。然而,返回值具有不同目的的事实并不能否定函数无论如何都被重载的事实。
这种事情常常会令人困惑,特别是当我们只看使用该函数的行时。看看第 10 行和第 11 行 —— 你会清楚地注意到返回的内容并不完全合乎逻辑。因此,如果您对代码的工作原理有疑问,请尝试研究其细节。不要以为仅仅因为一个函数或过程与您已知的另一个函数或过程具有相同的名称,它们就一定以相同的方式工作。但情况并非总是如此。
最后,我们可以对代码 06 做一个小改动。它显示在下面的代码片段中。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. long v = -16, 07. m[]; 08. bool b; 09. 10. // Add(m, Sum(10, 25)); 11. Print("Return: ", b = Sum(v, m)); 12. if (b) Print("Value: ", v); 13. ArrayFree(m); 14. } 15. //+------------------------------------------------------------------+ . . .
片段 01
请注意,在这段代码中,我将从代码中删除一行,并将其转换为注释。在这个例子中,这是第 10 行。但这个简单的变化,显然只意味着我们没有添加正确数量的元素进行分解,将导致如下所示的结果。

图 06
尝试对代码 06 进行其他更改,以获得其他类型的理解,因为这将在未来为您提供极大的帮助。
最后的探讨
也许这篇文章对新手程序员来说是最令人困惑的,因为在这里我表明,在同一段代码中,并非所有函数和过程都有唯一的名称。是的,我们可以很容易地使用同名函数和过程 —— 这被称为重载。虽然这是可以接受的,但在使用这种做法时应谨慎。因为它经常使代码不仅难以实现,而且以后也难以修复。
我希望这篇文章能为你提供一个解释,让你更加小心,不要自动假设一个已知的函数或过程必然会与使用重载实现的另一个函数或过程表现相同。
在附录中,我将留下本文中讨论的代码片段。尝试研究此材料,对应用程序中的代码进行更改,以了解重载如何影响您对代码的理解以及因滥用重载而可能出现的潜在错误。最好在机制仍然简单的时候学习如何处理错误,而不是试图在更复杂的系统中处理错误。因此,趁现在一切还比较简单,研究一下重载的问题。毕竟,随着我们继续前进,一切只会变得更加复杂。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15642
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
价格行为分析工具包开发(第十四部分):抛物线转向与反转工具
您应当知道的 MQL5 向导技术(第 52 部分):加速器振荡器
算法交易中的神经符号化系统:结合符号化规则和神经网络
MQL5中的自动化交易策略(第七部分):构建具备仓位动态调整功能的网格交易EA