English Русский Español Deutsch 日本語 Português
preview
从基础到中级:模板和类型名称(二)

从基础到中级:模板和类型名称(二)

MetaTrader 5示例 |
182 0
CODE X
CODE X

概述


在上一篇文章“从基础到中级:模板和类型名称(一) ”中,我们开始谈论一个相当复杂但非常有趣的话题:为函数和过程创建模板。由于这个主题很难在几篇文章中探讨和解释,因此我们将把它分成更多文章。然而,在继续讨论其他同样有趣的话题之前,我们不会单独深入探讨这个话题,因为有些事情只有在涉及其他话题的情况下才有意义。

但我们只会在这个主题上工作一段时间,直到我们建立了一个足够坚实和广泛的基础,以便我们可以在回到模板之前继续研究其他主题。毕竟这个话题非常广泛。无论如何,有必要澄清我们在上一篇文章中没有提到的几点,正是为了不使问题复杂化。我希望你能够轻松地学习这些文章中的每一篇,这样它们就能帮助你更正确、更安全地开始编程,至少对 MQL5 中的每个工具都有很好的理解。

虽然这里描述的大部分内容适用于其他语言,但必须适当注意这些概念的应用。

说完这些话后,是时候放松一下,排除可能的干扰,专注于本文将讨论的内容了。在这里,我们将更多地讨论模板。

此处提供的材料仅用于教学目的。在任何情况下,本应用程序都不应被视为最终版本,其目的不是研究所提出的概念。


模板,更多模板

模板最有趣的一点是,通过适当的规划,它们成为不可或缺的工具。这是因为我们最终创建了许多人所说的快速实现模型。换句话说,我们不再需要对所有内容进行编程,而只需要对必要元素的一部分进行编程。

最有可能的是,你不知道这是关于什么的。但是,当你学习和实践编程时,你会发现,如果你采取某些步骤,许多事情都可以更快地完成。了解和理解我们掌握的所有工具使我们能够选择最佳路径,不要执着于名字。试着理解公认的概念,随着时间的推移,你将能够把棋盘掌握在自己手中,建立自己的道路。

现在让我们从基于上一篇文章所示的一个非常简单的实现开始。如下所示。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(Sum(10, 25));
07.     Print(Sum(-10.0, 25.0));
08. }
09. //+------------------------------------------------------------------+
10. template <typename T>
11. T Sum(T arg1, T arg2)
12. {
13.     Print(__FUNCTION__, "::", __LINE__);    
14.     return arg1 + arg2;
15. }
16. //+------------------------------------------------------------------+

代码 01

在上一篇文章讨论的代码 01 中,我们有机会使用相同的函数来处理不同类型的数据。然而,委婉地说,这里有点烦人。问题在于第 06 行和第 07 行我们有不同类型的数据。在研究了上一篇文章的内容后,您可能已经注意到,编译器通过创建重载函数完美地解决了这个问题。因此,最终您不需要确定正在使用哪种数据类型。但这就是令人不快的部分所在:这里使用的数据类型必须相同,也就是说,如果传递给 Sum 函数模板的第一个参数是浮点类型,那么第二个参数也必须是浮点类型。否则,编译器将打印一个错误,或者充其量是一个警告,说明所使用的数据类型之间存在问题。

为了更好地理解,让我们改变其中一行。这可能是代码 01 的第 06 行或 07 行,以不同类型将传递给函数。请记住,该函数实际上尚不存在。编译器必须根据提供的模板来构建它。因此,我们将代码 01 更改为如下所示。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(Sum(10, 25));
07.     Print(Sum(5, 35.2));
08. }
09. //+------------------------------------------------------------------+
10. template <typename T> T Sum(T arg1, T arg2)
11. {
12.     Print(__FUNCTION__, "::", __LINE__);    
13.     return arg1 + arg2;
14. }
15. //+------------------------------------------------------------------+

代码 02

请注意,只有传递给函数的值发生了改变。在这种情况下,正如您所看到的,我们决定通过比较代码 01 和代码 02 来更改第 07 行。然而,尽管代码中发生了这种微小而无害的更改,看看我们试图编译它时发生了什么。

图 01

图 01 中显示的消息数据可能根据具体情况略有不同。读者可能会因为注意到编译器未能建立引用库而感到困惑,因为唯一的变化是代码第 07 行中的值。这正是我们之前谈到的令人恼火的事情。由于我们的例子纯粹是教学性的,人们可能会想,“为什么编译器不能根据提供的模板生成代码?”原因是,由于第一个参数是整数,我们不能在第二个参数中放置浮点值。反之亦然,当我们首先输入浮点值,然后输入整数值也是不行的。 

由于 Sum 函数模板是在代码 02 中定义的,因此编译器无法生成合适的函数来执行模板函数(即 Sum 函数)所期望的操作。许多初学者最终放弃了,选择了一种不同的方法来解决问题,因为一切都可以很简单地解决。然而,为了正确解决这个问题,必须首先了解用作模板的函数或过程的预期内容,以便创建其他重载的过程或函数。

由于我们的目标纯粹是教学目的,因此我们使用的函数非常简单。我们所期望的只是将两个值相加并返回其和的结果。如果您已经尝试将浮点值添加到整数值中,请记住结果将是浮点值。

这种事情被称为类型转换或类型转换,我们在讨论变量和常量时谈到了它。基本上,我们得到的是以下内容:

图 02

根据图 02,我们知道,在使用不同类型执行数学运算时,所有情况都使用 double 类型,就像代码 02 的第 07 行所做的那样。知道这一点,并且知道编译器使用模板而不是重载函数,我们可以让编译器明白我们知道发生了什么。因此,我们可以更改代码 02,使其实际工作,如下面的实现所示。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(Sum(10, 25));
07.     Print(Sum((double) 5, 35.2));
08. }
09. //+------------------------------------------------------------------+
10. template <typename T> T Sum(T arg1, T arg2)
11. {
12.     Print(__FUNCTION__, "::", __LINE__);    
13.     return arg1 + arg2;
14. }
15. //+------------------------------------------------------------------+

代码 03

请注意,在代码 03 中我们使用与代码 02 相同的代码。然而,在这里,我们强迫编译器理解,我们知道我们正在处理双精度类型,即使该值是整数类型的变量。这种显式类型转换是通过在代码 03 的第 07 行添加一个项来执行的,它将同一行 07 与代码 02 区分开来,并允许编译器使用 Sum 模板函数来创建适当的重载。因此,最终结果如下:

图 03

事实上,我们提出的解决方案并不是唯一可能的解决方案。我们可以做一些不同的事情,得到同样的结果。最主要的是要准确理解我们想要实现什么,以及我们目前的目标是什么。因此,我们不想仅仅记住代码,或者更糟糕的是,使用旧的 CTRL+C 和 CTRL+V 策略,而是想真正理解所使用的概念。这是我们解决所有与编程相关问题的唯一方法。

另一种解决方案是将其中一个参数限制为特定类型。尽管许多人认为这种方法不合适,但它允许我们在非常具体的情况下解决各种类型的问题,当我们提前知道我们将在特定的论证中始终使用某种类型的信息时。

因此,假设第一个参数始终具有整数类型,我们可以执行以下操作:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     Print(Sum(10, 25));
07.     Print(Sum(5, 35.2));
08. }
09. //+------------------------------------------------------------------+
10. template <typename T> T Sum(short arg1, T arg2)
11. {
12.     Print(__FUNCTION__, "::", __LINE__);    
13.     return arg1 + arg2;
14. }
15. //+------------------------------------------------------------------+

代码 04

该代码的结果与代码 03 的结果相同,即图 03。然而,在代码 04 中,我们告诉编译器模板函数的参数之一必须始终为整数类型。在这种情况下,我们使用有符号的 16 位类型,但它可以是任何其他类型。请注意,在这种情况下,我们不想显式执行类型转换。这是因为编译器已经知道在这种情况下该做什么。然而,重要的是要强调,第二个参数将由编译器在创建可执行文件的阶段确定。因此,我们将有一个函数来响应第 06 行中的调用,其中我们只使用整数类型,另一个函数来响应第 07 行中的调用,其中我们使用整数类型和浮点数。

现在,我想问你:在这样的场景中玩耍和练习难道不有趣吗?请注意,我们告诉编译器如何工作。通过这种方式,我们能够毫不费力地为开发同一件事创造不同的可能性。

但不要以为一切都结束了。还有另一种方法可以解决这个问题。然而,在这种情况下,事情会变得更加复杂,出现了一种不同的问题,那种解决方案将在另外的时候展示。尽管如此,它确实值得关注,因为它适用于各种情况,甚至可能为使用和使用 MQL5 打开一个新世界的大门。

然而,为了避免混淆,我们将在另一个主题中讨论这个问题。首先,学习这些概念,然后才尝试采用接下来会发生的事情。


一个模板,多种类型

在上一个主题中,我们讨论了如何处理一个相当不愉快的问题,这个问题有时会限制我们使用模板。然而,所研究的材料只是我们实际能做的第一部分。我们在这里讨论的内容并不常见,至少在 MQL5 中是这样,因为到目前为止,我不记得有人使用过类似的方法。很多人可能认为这样的机会不存在或者无法实现,这意味着他们将束手无策,无法实现他们的主要目标。

然而,你没有看到或听说过使用某物的事实并不意味着它不存在,也不意味着它在编程语言中不被接受。在许多情况下,问题甚至不在于此,而在于其他类型的问题,这些问题可能正是由于误用或(在大多数情况下)由于对与编程工具相关的一些概念的误解而产生的。

在上一篇文章中,我们解释了模板中使用的 T 实际上是一个标识符,编译器将使用它来本地识别特定类型。为了知道如何处理传入的信息,这是必要的。如果你理解标识符的概念,那么你就知道如果标识符声明正确,它将像变量或常量一样。

对于标识符,也就是我们在代码 04 第 10 行看到的 T,它不是一个变量,因为它在识别后不能被改变。“那么显然它是一个常数。”但是,它是一个编译器定义的常量,表示预期的数据类型。

请注意:当谈到数据类型时,我们指的是图 02 中的内容。因此,理解概念而不是记住公式或实现模型很重要。通过理解这个概念,虽然很简单,但(你很快就会注意到)它非常强大,我们可以创建一个几乎神奇的解决方案,了解如何使用模板来创建东西,无论是函数还是过程。要做到这一点,您需要添加尽可能多的标识符,以尽可能多地覆盖允许的情况。这个决定将在代码实现阶段由您做出。

然后,我们可以更改代码 04,找到与代码 02 更相似的东西,但没有代码 02 中发现的问题,在代码 02 中只能使用一种数据类型。这听起来很复杂,但实际上比你想象的要简单得多。让我们看看应用使用多个类型标识符的新概念,这个问题的解决方案会是什么样子。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define PrintX(X) Print(#X, " => ", X, "\n");
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     PrintX(Sum(10, 25));
09.     PrintX(Sum(5, 35.2));
10. }
11. //+------------------------------------------------------------------+
12. template <typename T1, typename T2> T1 Sum(T1 arg1, T2 arg2)
13. {
14.     Print(__FUNCTION__, "::", __LINE__);
15. 
16.     return (T1)(arg1 + arg2);
17. }
18. //+------------------------------------------------------------------+

代码 05

这就是事情变得非常复杂的地方,尽管我们很小心,逐渐展示了一切。然而,这里的问题恰恰在于我们在第 12 行中所做的工作,其中声明了 Sum 函数的模板。

执行后,代码 05 将导致下图的结果。

图 04

我们精确地突出了该图像中的一点,以吸引读者的注意力。请注意,操作结果不正确。相反,它与预期值不匹配,因为这里的预期值与图 03 中的相同。但是为什么呢?我们可以假设这种情况与第 12 行的记录方式相同,因为它显然不合理。然而,问题并不像您可能认为的那样出现在第 12 行,而是出现在第 09 行或第 16 行,这取决于您如何分析代码或如何实现它。因此,理解公认的概念并在随意编码之前进行思考非常重要。

你不一定要相信我。因此,我们不会触及第 16 行,但我们将通过更改声明值的顺序来修改第 09 行。这引出了下面提供的代码。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define PrintX(X) Print(#X, " => ", X, "\n");
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     PrintX(Sum(10, 25));
09.     PrintX(Sum(35.2, 5));
10. }
11. //+------------------------------------------------------------------+
12. template <typename T1, typename T2> T1 Sum(T1 arg1, T2 arg2)
13. {
14.     Print(__FUNCTION__, "::", __LINE__);
15. 
16.     return (T1)(arg1 + arg2);
17. }
18. //+------------------------------------------------------------------+

代码 06

请注意,我们只更改了我们所描述的内容,代码执行的结果如下。

图 05

“这是多么疯狂和毫无意义的行为!现在,在我更好地理解这个问题之前,我不敢开始编程。在我看来,这一切都很容易。我甚至认为自己是一名程序员,因为我能够编写出有效的小代码片段。但看着它,我意识到我仍然什么都不知道。我刚刚开始了解成为一名真正的程序员意味着什么。”

冷静点,毕竟没那么糟糕。事实上,一切并不总是像乍一看那么简单,尤其是当我们进入舒适区并没有离开它时。但问题是,许多人认为自己是优秀的程序员,因此干脆停止学习。因此,我确认: 

优秀的程序员永远不会停止学习。他不断更新知识,探索新概念。总是如此

您刚才看到的事情实际上会摧毁程序员的士气,尤其是当他拿到原则上正确的代码时。这基本上是正确的,然而,它总是毫无原因地返回不正确的结果。因此,不要欺骗自己:仅仅能够编写代码还不足以被视为程序员。要达到这个水平,你必须经历很多。在许多情况下,只有时间才能教会你。我自己也经历过,我告诉你:我花了很长时间才学会如何处理这些问题。我甚至开始理解为什么代码可能在某个时刻工作,在另一个时刻变得疯狂,乍一看会产生毫无意义的结果。

但是让我们弄清楚代码 05 和代码 06 中发生了什么,因为它们是相同的:唯一的区别是第 09 行中参数声明顺序的简单变化。

当编译器遇到我们在第 12 行声明的模板调用时,它会检查每个参数中使用的数据类型,就像以前一样,创建一个重载函数来服务于该特定调用,以防之前创建的函数都无法服务于当前模板。

由于我们在第 12 行有两个 typename 来指定两种不同的类型,因此我们可以使用两种完全不同的数据类型,这在以前是不可能的。这完美地涵盖了大量情况,因为事实上,值可以是整数或浮点数,我们一开始就不必担心。然而,由于具体问题,这种模拟并不能涵盖所有情况,但这超出了今天的主题范围。知道我们可以同时使用整数和浮点数据并且没有问题,我们可以得到一个几乎完美的模板。

因此,编译器将把第一个参数中声明的数据类型放入常量 T1 中,而将第二个参数中使用的数据类型放入常量 T2 中。然后,在第 16 行执行类型转换以匹配 Sum 函数的返回类型,我们可以得到类似于图 04 或 05 所示的结果。

由于在查看模板代码时,我们对编译器实际会组装什么只有一个模糊的概念,因此很难理解为什么仅仅通过修改第 09 行中声明值的顺序就得到了如此不同的结果。

为了更好地理解,让我们看看编译器在这两种情况下编写的函数是什么。然后,如果假设我们不使用模板,而是使用传统编码,我们将得到以下代码 05:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define PrintX(X) Print(#X, " => ", X, "\n");
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     PrintX(Sum(10, 25));
09.     PrintX(Sum(5, 35.2));
10. }
11. //+------------------------------------------------------------------+
12. int Sum(int arg1, int arg2)
13. {
14.     Print(__FUNCTION__, "::", __LINE__);
15. 
16.     return (int)(arg1 + arg2);
17. }
18. //+------------------------------------------------------------------+
19. int Sum(int arg1, double arg2)
20. {
21.     Print(__FUNCTION__, "::", __LINE__);
22. 
23.     return (int)(arg1 + arg2);
24. }
25. //+------------------------------------------------------------------+

代码 07

当执行代码 07 时,您将得到与图 04 中所示完全相同的结果。如果代码 06 是在传统的编程模型中创建的,那么它的内部内容将如下所示。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #define PrintX(X) Print(#X, " => ", X, "\n");
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     PrintX(Sum(10, 25));
09.     PrintX(Sum(35.2, 5));
10. }
11. //+------------------------------------------------------------------+
12. int Sum(int arg1, int arg2)
13. {
14.     Print(__FUNCTION__, "::", __LINE__);
15. 
16.     return (int)(arg1 + arg2);
17. }
18. //+------------------------------------------------------------------+
19. double Sum(double arg1, int arg2)
20. {
21.     Print(__FUNCTION__, "::", __LINE__);
22. 
23.     return (double)(arg1 + arg2);
24. }
25. //+------------------------------------------------------------------+

代码 08

正如代码 07 会产生图 04 一样,代码 08 会产生图 05。请注意,一个代码和另一个代码之间的差异非常微妙,在许多情况下,它可能会被忽视,我们甚至不会意识到出了什么问题。然而,与使用模板的代码不同,在代码 07 和 08 中我们将很快找出问题的原因。查看结果,您可以看到这里有一个显式的类型转换,这会导致代码传递错误的响应,从而纠正问题。

然而,在使用模板时,并不容易注意到。在这种情况下,你会很难,甚至可能拒绝继续这条路。只有你有足够的意志力,你才能最终找出错误所在。然而,在您思考之前,“所以,我们要做的就是使用显式类型转换在代码 05 或代码 06 的第 16 行进行双精度类型转换。这肯定能解决我们在终端收到的响应的问题。”

让我们看看为什么这并不能真正解决问题。在某些情况下,它甚至会产生其他问题。原因很简单。问题是,您将尝试在代码的第 09 行中求解结果,而忘记了在第 08 行中我们使用整数类型的数据。然后,您可能会进一步混淆情况,从而在其他地方产生新问题,而不是解决问题。

这当然是一种典型的情况,最终会变得相当尴尬。因此,很难找到使用多种类型模板的实用代码,至少在 MQL5 中是这样。至于 C 语言,主要是 C++,这样的事情很常见,而且时有发生。

因此,这里介绍的内容与其说是日常使用,不如说是有趣。然而,当有必要使用它时,你会很快记住它可能会产生什么潜在的问题。因此,请随时提出解决此类冲突的解决方案。据我所知,解决这个问题没有简单的方法。即使在 C++ 中,如果发生这种情况,您也常常需要摆脱困境来解决问题。相信我,这一点都不好玩。


最后的探讨

本文解释了如何处理您可能遇到的最令人恼火和最困难的编程情况之一:在同一个函数或过程模板中使用不同的类型。尽管我们大部分时间只关注函数,但这里涵盖的一切都是有用的,可以应用于过程,无论是在使用按值传递还是在使用引用传递的情况下。然而,为了不让材料变得乏味,我们不会在实际中展示这些案例。

因此,让我们一致认为,您应该练习并尝试编写小代码片段来探索我们刚才讨论的那些案例,例如使用分步链接和使用过程模板,因为在这个早期阶段,我们不一定会使用这些。

在应用程序中,您将找到今天提供的许多代码。不可用的代码是对应用程序中提供的代码的简单修改。无论如何,练习这里显示的内容将有助于更好地掌握内容。在下一篇文章中,我们将更多地讨论模板。因此,很快就会见到你!

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

附加的文件 |
Anexo.zip (1.84 KB)
在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法 在 MQL5 中自动化交易策略(第 13 部分):构建头肩形态交易算法
在本文中,我们将自动化 MQL5 中的头肩形态。我们分析其架构,实现一个用于检测和交易该形态的 EA,并对结果进行回测。这个过程揭示了一个具有改进空间的实用交易算法。
数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易 数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易
当与机器学习模型共事时,确保用于训练、验证和测试的数据一致性必不可少。在本文中,我们将创建我们自己的 MQL5 版本 Pandas 函数库,确保使用统一方式来处理机器学习数据;这样做是为确保在 MQL5 内部和外部应用相同的数据,其中大部分发生在训练阶段。
MQL5中的高级内存管理与优化技术 MQL5中的高级内存管理与优化技术
探索在MQL5交易系统中优化内存使用的实用技巧。学习构建高效、稳定且运行速度快的智能交易系统(EA)和指标。我们将深入探究MQL5中内存的实际运作方式、致使系统运行变慢或出现故障的常见陷阱,以及——最为关键的是——如何解决这些问题。
日内交易:拉里·康纳斯(Larry Connors)RSI2均值回归策略 日内交易:拉里·康纳斯(Larry Connors)RSI2均值回归策略
拉里·康纳斯(Larry Connors)是知名交易员与量化交易领域权威作家,其最著名的成果之一是2周期相对强弱指数(RSI2)策略。该指标通过捕捉短期超买超卖信号,辅助判断市场反转时机。在本文中,我们将首先阐述研究契机,随后在MQL5中复现康纳斯的三大经典策略,并应用于标普500指数差价合约(CFD)的日内交易场景。