English Русский Español Deutsch 日本語 Português
preview
从基础到中级:数组和字符串(二)

从基础到中级:数组和字符串(二)

MetaTrader 5示例 |
30 0
CODE X
CODE X

概述

此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。

在上一篇文章从基础到中级:数组和字符串(一)中,我们对字符串和数组进行了简要介绍,尽管实际上那篇文章几乎完全关注字符串。其中涵盖的所有内容都只是对该主题的简短介绍。然而,为了让我们转向其他主题,我们必须首先更深入地研究前面的解释。因此,遵循和理解本文所提供材料的前提是已经理解了前一篇文章的内容。

除了这个初始先决条件外,还有其他同样重要的先决条件,例如理解 IF 语句和循环构造,以及对运算符和变量有一些了解。如果您已经学习了前面的文章,那么您应该对所有这些主题都很熟悉。如果不是这样,建议您查看前面的材料,以充分掌握这里将讨论的内容。

我们现在可以开始讨论本文的主题了。这是我们的新主题。


自定义格式

MQL5 编程最令人愉快的一个方面是,我们通常不需要担心从头开始创建某些东西。事实上,MQL5 标准库很少不能满足我们的需求。然而,在某些情况下,它可能并不完全符合我们的要求或想要做的事情。其中一种情况是创建自定义格式来显示某些类型的信息,无论是在终端中还是在我们打算使用的图形对象中。

由于在这个阶段,我们还没有使用图形对象,我们所有的任务都将集中在使用 MetaTrader 5 的标准输出,换句话说,就是终端。我很快就会教你如何让事情变得更有趣。但在那一刻,我们首先需要对编程语言的一些关键特性建立一个坚实而统一的理解。

因此,这个初始内容并不完全与 MQL5 绑定。它也可以应用于其他编程语言,特别是处理编程概念和规则的部分。正如本主题开头提到的,在某些情况下,MQL5 标准库是不够的。但是,如果你使用前几篇文章中的知识和一点逻辑思维,你可以实现许多有趣的事情,即使乍一看,我们只涵盖了似乎缺乏明确目的的基本主题。

为特定知识赋予目的的行为本身并不是程序员最令人兴奋的部分。真正有趣的部分在于,当你开始理解事物是如何运作的,并开始思考如何应用这些知识来解决特定问题的时候。这才是学习真正有意义的时候。

现在,我希望你考虑以下问题。假设您想在 MetaTrader 5 终端中查看或打印二进制值。原因并不重要,重要的是问题本身。因此,当你面临这一挑战时,你会立即想到:“好吧,我可以使用字符串格式在终端中显示二进制值”。完美,这是一个优秀程序员的第一个想法。然而,一个更有经验的程序员已经知道答案了。另一方面,初学者可能会在文档中搜索一种格式化字符串的方法,以显示数值的二进制表示,但会发现这种功能并不存在。正是在这个时刻,我们需要创造力与知识相结合。

由于不存在显示二进制值的内置格式,我们需要创建自己的格式。如果你是编程新手,并且只具备前几篇文章中涵盖的知识,你可能会认为这是不可能的。但亲爱的读者,事实真是如此吗?让我们看看是否可以仅使用前面文章中提供的信息来完成。

我们知道标准库中有一个函数允许我们格式化各种类型的数据。我们需要做的就是创建自己的格式来处理二进制值。这是最简单的部分,但还有一项更困难的任务。与允许向函数传递无限数量参数的 C 或 C++ 不同,MQL5 不提供这种灵活性。据我所知,C 和 C++ 是唯一允许这种功能的语言,汇编语言也是如此,但说实话,没有人会疯狂到用汇编语言写东西。坦率地说,这是一种疯狂的表现。

好了,无法向函数传递无限数量的参数使得事情一开始看起来更加复杂。但我们可以将问题分解为可管理的步骤。由于我们想创建一种新格式,同时仍使用 MQL5 库中的现有格式,因此我们可以相应地操纵数据。如何做呢?很简单,亲爱的读者。我们创建了一个函数,返回一个包含值的二进制表示的字符串。由于 MQL5 标准库在格式化输出时可以处理字符串,因此很容易定义我们的自定义格式。

这样想,这似乎是一项艰巨的任务。但通过查看下面的代码,你会发现它有多简单:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     uchar value = 138;
07. 
08.     PrintFormat("The value %d in binary is written as: %s", value, BinaryToString(value));
09. }
10. //+------------------------------------------------------------------+
11. string BinaryToString(const uchar arg)
12. {
13.     return "--BINARY--";
14. }
15. //+------------------------------------------------------------------+

代码 01

当我们运行代码 01 时,结果将是这样的:

图 01

此时,你可能会想,“哇,但这不是我们想要实现的”。确实,这不是我们想要的。但这向你表明,我们可以在现有的基础上继续开发,并尝试创造新的东西。请注意,第 13 行中的文本字符串与图像 01 中的相同。换句话说,它有效。

现在我们所要做的就是将函数接收到的值转换为二进制表示。然后,我们将此表示插入到字符串中并返回它。我们在第 13 行执行此操作。

我们如何将结果值转换为仅包含 0 和 1 的字符串?亲爱的读者,这一切都非常简单。为此,我们使用了一个循环。然而,有一个小问题。但我将在代码中直接解释它,如下所示:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     uchar value = 138;
07. 
08.     PrintFormat("The value %d in binary is written as: %s", value, BinaryToString(value));
09. }
10. //+------------------------------------------------------------------+
11. string BinaryToString(const uchar arg)
12. {
13.     string  sz0 = "";
14.     uchar   mask = 0x80;
15. 
16.     while (mask)
17.     {
18.         sz0 = sz0 + ((arg & mask) == mask ? "1" : "0");
19.         mask = mask >> 1;
20.     }
21. 
22.     return sz0;
23. }
24. //+------------------------------------------------------------------+

代码 02

当您运行此代码时,您将在 MetaTrader 5 终端中看到以下输出:

图 02

这绝对是完美的,换句话说,我们用最少的基本知识完成了任务。然而,这段代码中有一个小细节。根据到目前为止所展示的知识,我们不能使用任何类型的数据将其转换为二进制表示。如果不深入了解更高级的编程概念,这是不可能的。也就是说,对于我们使用单一数据类型的简单情况 —— 在本例中为 uchar。我们已经有一个有效运行的函数,正如您所见,它非常简单。由于这里所做的一切都已在之前的文章中介绍过,因此我无需解释此函数如何执行其任务。然而,有一个我们尚未讨论的运算符正在使用:移位运算符。既然它出现在函数中,我认为解释一下它是如何工作的是很好的。这将是下一节的重点。但在我们继续之前,我想提请您注意一个重要细节: 'mask' 变量必须具有与 'arg' 变量相同的位宽度。这确保了值的正确和准确的转换和翻译。随着更高级的文章的发表,我们将学习如何让编译器为我们处理这方面的问题。这将允许我们使用从最复杂到最简单的任何数据类型,而无需在代码 02 中显示的函数中进行太多修改。但那是未来的话题。现在,让我们了解移位运算符的工作原理。那么让我们转到一个新的主题。


移位运算符

操作符通常是不言自明的。然而,移位运算符可能会让一些人感到有点困惑,因为它主要用于计算环境。据我所知,它并没有出现在计算机科学或数字电子领域之外。您可以在代码 02 的第 19 行看到移位运算符,它非常实用,在各种情况下都非常有用。然而,它不是一个特殊的运算符,因为它只是其他操作的简写,通常会替换您在某些代码中可能遇到的两个更冗长的运算符。我说“两个”是因为确实有两个移位运算符:一个用于向右移位,一个用于向左移位。许多初学者将这些移位运算符与用于比较值的关系运算符(如大于或小于)混淆。这种混淆通常源于缺乏关注,因为移位运算符由双箭头(左或右)组成,而关系运算符使用单箭头。

那么,移位运算符到底起什么作用?当使用时,它告诉我们将给定值向左或向右推送多少位。这听起来可能有点复杂,但实际上比它所取代的操作简单得多。 

但这意味着什么?我不明白。如果有另一种方式来表达同样的事情,我们为什么不直接用它来代替呢?这是一个合理的问题。我可以给你看。事实上,这正是我写这一节的原因。我希望你明白,通过不同的实现也可以实现相同的结果。说两个程序员对一个问题提出了完全相同的解决方案是不太可能的。这并非不可能,但绝对罕见。这是因为解决问题的方式通常取决于每个程序员的知识和经验水平。

现在,让我们回到我们的重点。为了以清晰简单的方式解释移位运算符的工作原理,我们将使用下面显示的代码。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const uchar shift = 1;
07.     uchar   i0 = 153,
08.             i1,
09.             i2,
10.             i3,
11.             i4;
12. 
13.     i1 = i0 << shift;
14.     i2 = i0 * (uchar)MathPow(2, shift);
15.     i3 = i0 >> shift;
16.     i4 = i0 / (uchar)MathPow(2, shift);
17. 
18.     PrintFormat("Shift value          : %d\n" + 
19.                 "Original             : %s\t%d\n" +
20.                 "Shifted to the left  : %s\t%d\n" +
21.                 "Multiplication result: %s\t%d\n" +
22.                 "Shifted to the right : %s\t%d\n" +
23.                 "Division result      : %s\t%d"
24.                 , shift, BinaryToString(i0), i0, 
25.                          BinaryToString(i1), i1, 
26.                          BinaryToString(i2), i2, 
27.                          BinaryToString(i3), i3, 
28.                          BinaryToString(i4), i4
29.                 );
30. }
31. //+------------------------------------------------------------------+
32. string BinaryToString(const uchar arg)
33. {
34.     string  sz0 = "";
35.     uchar   mask = 0x80;
36. 
37.     while (mask)
38.     {
39.         sz0 = sz0 + ((arg & mask) == mask ? "1" : "0");
40.         mask = mask >> 1;
41.     }
42. 
43.     return sz0;
44. }
45. //+------------------------------------------------------------------+

代码 03

执行此代码时,您将看到下图所示的输出:

图 03

亲爱的读者,请密切注意。这个代码 03 起初可能看起来很混乱,但实际上非常清晰,它有效地实现了这一目的。但要完全理解它,您需要知道 MathPow 函数的作用。由于它是 MQL5 标准库的一部分,我建议查看官方文档以了解更多详细信息。简而言之,它只是执行指数运算。但是,它返回一个双精度值。由于我们的数据是 uchar 类型,我们显式地转换了该类型。这将防止编译器抱怨数据类型不匹配。

现在,请注意,在图像 03 中,我们在每一行上都显示值及其二进制表示形式。这种二进制表示很重要,因为它使我们的原始值发生了什么变得更加清晰。你可能会说,向左移动一个值会将其乘以 2,向右移动会将其除以 2。在某种程度上,您没有错:也就是说,如果我们只分析图 03。但是如果我们将移位次数从 1 改为 3,会发生什么情况呢?

图 04

图 04 显示了当我们移动 3 位时发生的情况。在这种情况下,你可能会注意到一些奇怪的事情。移动 3 位并不意味着我们简单地乘以 2 或除以 2,而是意味着我们在处理 2 的幂。也就是说,我们将给定的数字除以或乘以 2。通过使用移位运算符,可以精确地获得正确的结果。由于执行这种操作(先进行指数运算,然后进行乘法或除法)的计算成本更高,因此最好使用移位运算符。

然而,当查看图 04 中的值时,您可能会注意到部分信息消失了。亲爱的读者,这不是程序中的错误。这是数据类型限制的结果。MQL5 是一种强类型语言,当我们超出类型的限制时,一些信息就会丢失。有多种方法可以避免或解决这个问题,但这不是我们目前的重点。我们将介绍如何在更合适的时间处理这些情况。相信我,在编程方面,总有更多的学习空间。

我相信现在我们已经清楚移位运算符的工作原理了。了解了这些之后,代码 02 现在应该很容易理解了。我们现在可以转向另一个主题,它仍然与字符串和数组有一定的联系。


全字母句和字母重排句

即使我们目前所涵盖的材料有限,我们现在也可以探索一个有趣甚至好玩的应用程序。我指的是全字母句。全字母句子包含字母表中每个字母,通常用于测试目的。虽然这是一个相当古老的概念,但它提供了一个极具教育性和吸引力的练习。也许最著名的全字母句是:

A QUICK BROWN FOX JUMPS OVER THE LAZY DOG 0123456789

这句话最初是用来测试旧电报系统的。它包括消息中可能出现的所有字符。然而,更出名的是微软在 Word 中使用它来预览文本的打印方式。当时,一些人甚至声称微软工具中有隐藏的信息,这让许多用户不敢使用电脑。

但即使在我们现在讨论的背景下,我们仍然可以使用这种短语。这将允许学习更多关于字符串和数组的知识。它很简单,也很有趣,我相信你会喜欢学习它是如何工作的。

经常听到人们说我们应该使用强密码,甚至完整的短语作为密码。但是创建强密码或记住它们通常是一项艰巨的任务。另一方面,许多人建议使用专门的软件来存储密码。尽管如此,如果这些工具需要主密码才能访问所有其他工具,我个人认为这并不是特别有帮助。

然而,即使是一个了解基本函数和命令工作原理的初学者程序员,也可以构建一个简单的工具来生成相对强大的密码。当然,密码的强度将取决于系统及其设计方式。但是,亲爱的读者,只要有一点创造力,你肯定能想出一些非常有趣的东西,尤其是因为这里的目标纯粹是教育性的。

让我们首先考虑以下问题:什么是密码?好吧,密码本质上是一组存储在字符串中的可打印字符。因此,如果我们取一个字符串并对其进行操作,我们可以创建一个对字符串进行混洗或加密的程序。由于目标是创建一个强密码,这种混洗可能是一种加密形式,也可能只是一个字谜,可以有效地隐藏(至少部分隐藏)原始密码。也就是说,字谜并不是隐藏敏感信息的最佳方式,比如将在其他地方使用的密码。在这种情况下,混洗或加密是更好的选择,使用字谜或全字母拼词作为基本短语。密钥就是一句你能轻易记住的句子,最终的结果就是你使用的密码。

为了帮助您更好地理解我所描述的内容,让我们来看一段简单而朴实的代码。您可以在下面看到它:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const string sz_Secret_Phrase = "Um pequeno jabuti xereta viu dez cegonhas felizes";
07. 
08.     Print("Result:\n", PassWord(sz_Secret_Phrase));
09. }
10. //+------------------------------------------------------------------+
11. string PassWord(const string szArg)
12. {
13.     const string szCodePhrase = "The quick brown fox jumps over the lazy dog";
14. 
15.     string  szPsw = "";
16.     uchar   pos;
17. 
18.     for (int c = 0; c < StringLen(szArg); c++)
19.     {
20.         pos = (uchar)(StringGetCharacter(szArg, c) % StringLen(szCodePhrase));
21.         szPsw = StringFormat("%s%c", szPsw, StringGetCharacter(szCodePhrase, pos));
22.     }
23. 
24.     return szPsw;
25. }
26. //+------------------------------------------------------------------+

代码 04

看看这段代码,你可能会想:你到底想在这里做什么?放松,亲爱的读者。乍一看,我们在这里做的事情可能有点疯狂和荒谬。但很快你就会明白我想给你看什么。请考虑以下内容:此代码仅使用我们到目前为止所涵盖的内容。这里没有介绍什么新东西。如果你已经阅读了前面的文章,并花时间研究了本例中使用的库函数,那么一切都可以完全理解。这里的一切都依赖于 MQL5 文档中很容易找到的功能。因此我不会详细介绍所使用的函数。但是,当您不做任何修改地运行此代码时,您将看到以下输出:

图 05

图 05 中突出显示的区域代表您应该使用的密码。但请注意,由于空间字符的存在,它有点缺陷。通常,密码不包含空格。所以,我们需要纠正这个问题。解决这个问题的一个非常简单的方法是删除空格并将第一个字母大写。这很容易实现。对代码 04 的第 13 行进行此更改后,我们将得到类似于下面的代码:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const string sz_Secret_Phrase = "Um pequeno jabuti xereta viu dez cegonhas felizes";
07. 
08.     Print("Result:\n", PassWord(sz_Secret_Phrase));
09. }
10. //+------------------------------------------------------------------+
11. string PassWord(const string szArg)
12. {
13.     const string szCodePhrase = "TheQuickBrownFoxJumpsOverTheLazyDog";
14. 
15.     string  szPsw = "";
16.     uchar   pos;
17. 
18.     for (int c = 0; c < StringLen(szArg); c++)
19.     {
20.         pos = (uchar)(StringGetCharacter(szArg, c) % StringLen(szCodePhrase));
21.         szPsw = StringFormat("%s%c", szPsw, StringGetCharacter(szCodePhrase, pos));
22.     }
23. 
24.     return szPsw;
25. }
26. //+------------------------------------------------------------------+

代码 05

此时,比较代码 05 和代码 04,我们可以说:“什么都没有改变。这行不通。结果将与之前的结果非常相似。”但这是真的吗?让我们来一探究竟。当执行代码 05 时,输出如下:

图 06

哇!它变了,而且变了很多。这完全不同。它看起来与图 05 中显示的完全不同。许多人认为,要编写这样的程序,你必须是一个数学天才或一个有多年实践经验的开发人员。然而,我们在这里看到的是,即使对编程只有基本的了解,再加上健康的创造力,你也可以取得非常令人印象深刻,有时甚至令人惊讶的结果。现在,如果你发现这个密码仍然太简单,即使它很长,我们也可以很容易地修复它。请记住,代码 04 和代码 05 之间的唯一区别是第 13 行。其余行均未修改。很自然地,你可能会开始想:如果我们在第 13 行添加一些额外的符号会怎样?这会使密码更强吗?好吧,亲爱的读者,我们也试试吧。现在,让我们将第 13 行更新为下面显示的版本。

    const string szCodePhrase = ")0The!1quick@2brown#3fox$4jumps%5over^6the&7lazy*8dog(9";

输出将是:

图 07

这难道不是我们正在进行的一个有趣的小项目吗?我们在这里使用非常基本的编程。但我们可以使事情更加精细,尽管这需要一种稍微不同的方法。

这一次,我将做一些不同的事情来帮助你理解上一篇文章中提到的细节。在那里,我注意到字符串类型在 C 或 C++ 中不存在。然而,在 MQL5 中,这种类型确实存在,特别是为了避免我们依赖 C 和 C++ 中使用的编程方法。在 C 和 C++ 中,字符串本质上是一个数组,它包含一个特殊值,指示字符串的结束位置。由于 MQL5 出于相同的目的使用相同的值,我们可以调整密码生成代码,使其不再依赖于 MQL5 标准库调用。

现在,我希望你密切关注。我们将在 MQL5 中使用直接来自 C 和 C++ 的某些内容。这就是为什么我说 MQL5 处于一种中间地带,正如我在上一篇文章中所解释的那样。现在,我将简要介绍这个主题,我们将在下一篇文章中更深入地探讨它。


字符串是数组

本节的标题确切地说明了编程过程中到底发生了什么。我意识到起初这听起来可能很困惑。但在下一篇文章中,我们将更深入地探讨这个话题。现在,我想把这个概念留给你,这样你就可以早点开始研究了。现在,我想把这个概念留给你,这样你就可以早点开始研究了。

利用这一时刻,我们可以更新上一节中显示的代码,将字符串视为数组。这可能看起来很奇怪,但从编程的角度来看,以这种方式处理它有几个优点。

因此,如前所述,代码 04 变为如下所示的版本:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     const string sz_Secret_Phrase = "Um pequeno jabuti xereta viu dez cegonhas felizes";
07. 
08.     Print("Result:\n", PassWord(sz_Secret_Phrase));
09. }
10. //+------------------------------------------------------------------+
11. string PassWord(const string szArg)
12. {
13.     const string szCodePhrase = "The quick brown fox jumps over the lazy dog";
14. 
15.     uchar   psw[],
16.             pos;
17. 
18.     ArrayResize(psw, StringLen(szArg));
19.     for (int c = 0; szArg[c]; c++)
20.     {
21.         pos = (uchar)(szArg[c] % StringLen(szCodePhrase));
22.         psw[c] = (uchar)szCodePhrase[pos];
23.     }
24.     return CharArrayToString(psw);
25. }
26. //+------------------------------------------------------------------+

代码 06

当您运行代码 06 时,输出将与图 05 中显示的内容相同。但是,如果您修改第 13 行,如前所述,您会注意到结果与前面的结果保持一致。也就是说,代码 06 带来了一些优势,因为我们现在使用基于数组的系统而不是直接使用字符串。下一篇文章将更好地解释这种方法的好处,我将更详细地介绍如何利用这种建模。不管你信不信,亲爱的读者,这段代码仍然很简单,任何有专门学习习惯的受过良好指导的初学者都能理解它,而我甚至不必解释它是如何工作的。

无论如何,我将在下一篇文章中详细解释这段代码。我现在给你看只是为了确保材料不会开始堆积。不要以为仅仅因为它看起来简单易行,你就可以把它留到以后。


最后的探讨

在我看来,这篇文章很有趣,我们创造了一些实用的东西,其真正的目的不仅仅是应用理论概念。我知道我们谈到的一些问题起初可能看起来令人望而生畏。但这不应该成为放弃的理由。恰恰相反,我想在这里证明的是,即使是一个有创造力和一点聪明的初学者程序员,也可以构建和操纵文本数据来创建许多人认为需要多年编程经验的东西。

我相信,只使用基本知识来应用这些概念 —— 因为这里的一切都是基于前面文章中所涵盖的内容 —— 会让你更投入,更不被动。希望现在很清楚,你到目前为止学到的材料是有用和相关的。就是这些,因此,请尽情探索附件。我们下篇文章再见,我们将讨论数组。

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

附加的文件 |
Anexo.zip (2.43 KB)
基于MQL5的自动化交易策略(第一部分):Profitunity系统(比尔·威廉姆斯的《交易混沌》) 基于MQL5的自动化交易策略(第一部分):Profitunity系统(比尔·威廉姆斯的《交易混沌》)
在本文中,我们研究了比尔·威廉姆斯(Bill Williams)的Profitunity系统,深入剖析其核心组成部分以及在混沌市场中独特的交易方法。我们指导读者在MQL5中实现该系统,专注于自动化关键指标和入场/出场信号。最后,我们对策略进行测试和优化,提供其在不同市场环境下的表现。
Connexus观察者模式(第8部分):添加一个观察者请求 Connexus观察者模式(第8部分):添加一个观察者请求
在本系列文章的最后一篇中,我们探讨了观察者模式(Observer Pattern) 在Connexus库中的实现,同时对文件路径和方法名进行了必要的重构优化。该系列文章完整地记录了Connexus库的开发过程——这是一个专为简化复杂应用中的HTTP通信而设计的工具库。
交易中的神经网络:具有相对编码的变换器 交易中的神经网络:具有相对编码的变换器
自我监督学习是分析大量无标签数据的有效方法。通过令模型适应金融市场的特定特征来提供效率,这有助于提升传统方法的有效性。本文讲述了一种替代的注意力机制,它参考输入之间的相对依赖关系。
原子轨道搜索(AOS)算法:改进与拓展 原子轨道搜索(AOS)算法:改进与拓展
在本文的第二部分,我们将继续开发一种改进版的原子轨道搜索(AOS)算法,重点聚焦于特定操作符的优化设计,以提升算法的效率和适应性。在分析了该算法的基本原理和运行机制之后,我们将探讨提升其性能以及分析复杂解空间能力的方法,并提出新的思路以扩展其作为优化工具的功能。