
从基础到中级:运算符优先级
概述
此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
在上一篇文章“从基础到中级:FOR 语句”中,我们介绍了 FOR 语句的基础知识。使用前面文章中提供的材料,您可以创建相当数量的 MQL5 代码。即使这些是非常简单的应用程序,它们也可以成为许多人自豪和享受的源泉。
对于许多其他程序员来说,初学者创建的小代码片段似乎微不足道,但如果初学者找到了所遇到问题的解决方案,这确实是一个值得骄傲的理由。说实话,到目前为止,我们讨论的内容只允许您创建一些类似脚本的代码。即使这是一件非常简单的事情,不需要互动,如果你自己做过,那么你已经开始应用基本知识,并朝着正确的方向前进。
但现在是时候转向一个新的主题了。本主题将使我们能够进一步创建更有意义的代码。今天我们将探索运算符。尽管我们之前已经讨论过它们,但现在我们将进一步探讨,因为我们之前讨论的内容相当基础和简单。在本文中,我们将深入研究实践中的运算符优先级规则,以及三元运算符,对许多人来说,这是一个有点令人困惑的概念,但在各种情况下都非常有用。它可以在某些编程任务中为我们节省时间和精力。
理解本文内容的先决条件是了解如何在 MQL5 代码中声明和使用变量。这个话题在之前的文章中已经讨论过了。如果您还没有这些知识,请在继续阅读之前参考前面的文章。现在我们开始本文的第一个主题。
优先级规则
理解和熟悉优先级规则非常重要,我在另一篇文章中提到过这一点。但在这里,我们将更深入地探讨这个话题。
当查阅优先级规则的文档时,您会发现一个概述这些规则的表格。然而,许多人未能正确理解规则中包含的含义或信息。如果是这样的话,亲爱的读者,没有必要感到尴尬或犹豫。当第一次遇到这种信息时,感到有些困惑是完全正常的。毕竟,在学校里,作为程序员,我们通常不会以在这里应用的方式学习东西。
尽管在某些情况下,某些程序员构建某些表达式的方式乍一看可能会令人困惑,但它们在技术上通常是正确的。即使结果对我们来说出乎意料。这就是为什么理解优先级规则如此重要。你不需要记住它们。通过不断的使用和练习,你会习惯的。但最重要的是:
如果你不能理解自己的代码,其他人当然也不会。
这就是为什么你应该始终考虑别人如何解释你试图构建的内容。因此,让我们从以下几点开始:本主题开头提到的表格应该从上到下阅读。顶部列出的运算符具有更高的执行优先级。随着您在表格中向下移动,优先级逐渐降低。
然而,这里有一个小细节需要记住。为了解释这一点,让我们看看下图中的表格:
图 01
在这里,您可以看到运算符按特定顺序列出。注意这个顺序非常重要。您还会注意到它们被分为不同的类别。这就是许多人容易感到困惑的地方。这是因为当我们遇到某些代码时,如果我们不理解图 01 所示的分组,我们就无法预测表达式的结果。幸运的是,这种分组实际上很容易理解,而且它使事情变得容易得多,所以你不需要记住所有的优先级规则。首先,我们有引用运算符。这些优先于所有其他元素,因为它们决定了我们如何访问特定元素。紧接着,我们遇到了类型或二元运算符。在这些情况下,应该从右到左阅读代码。但这是为什么呢?别担心,我们会明白这一点。我将详细解释在这种情况下如何阅读代码。现在,请注意,这与从左向右读取的引用运算符不同。
我知道这听起来很令人困惑。此时,你可能会想:“他们为什么把事情弄得这么复杂?”但亲爱的读者,这并不是为了复杂化。在实践中,这一切都很有意义。如果没有看到它的实际动作,你可能会觉得自己走进了疯人院。但在第三组中,情况变得更加清晰,其中包括大多数人更熟悉的元素。这些是基本的算术运算符,在这种情况下,代码是从左向右读取的。这种模式一直延续到图 01 的其余部分。
现在,让我们看看这一切在实践中是如何运作的。为此,我们将使用一些非常简单明了的代码片段,在那里我们只需打印一些值。这是有趣和容易的部分。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print("Factoring: { ", #X, " } is: ", X) 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. char value = 9; 09. 10. PrintX(value); 11. PrintX(++value * 5); 12. PrintX(value); 13. } 14. //+------------------------------------------------------------------+
代码 01
所以现在我问你,亲爱的读者:终端上会打印什么值?如果不清楚运算符优先级,您可能会猜测 46,因为我们将一个变量(值为 9)乘以一个常数(值为 5),然后加 1。但这是错误的。运行代码 01实际结果为 50,如下图所示:
图 02
这看起来令人困惑吗?那么,我们再尝试一个小实验,这次只改变代码中的一个小细节,怎么样。您可以在下面看到它:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print("Factoring: { ", #X, " } is: ", X) 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. char value = 9; 09. 10. PrintX(value); 11. PrintX(value++ * 5); 12. PrintX(value); 13. } 14. //+------------------------------------------------------------------+
代码 02
代码 02 的结果如下:
图 03
我在上一篇文章中不是说过我们会玩得很开心吗?亲爱的读者,我可以在很长一段时间内继续玩弄这些优先规则,向你展示,就在你认为自己已经想通了,相信自己已经准备好挑战世界的那一刻,你实际上只是刚刚站起来……就要迈出第一步了。
我知道,你甚至不需要告诉我,这一切似乎都很疯狂。你可能认为我一定是个疯子。但相信我,亲爱的读者,乐趣才刚刚开始。事情会变得更加有趣。那么,我们来看看一段更有趣的代码怎么样?让我们从下面显示的开始:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print("Factoring: { ", #X, " } is: ", X) 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. char v1 = 9, 09. v2 = 5; 10. 11. PrintX(v1); 12. PrintX(v2); 13. PrintX(v1 & 1 * v2); 14. PrintX((v1 & 1) * v2); 15. } 16. //+------------------------------------------------------------------+
代码 03
漂亮的代码。它真的很令人愉快,尤其是当它在循环中运行时。以下是它产生的输出。
图 04
现在承认吧:你和我一样,看到这种事情很开心,不是吗?请注意,结果如何取决于表达式中括号的存在与否。
在写这种表达式时,实际上有一个普遍的规则。这不是一个严格的或正式记录的规则,但它存在于程序员中。规则如下:
编写复杂表达式时,请尝试使用括号将其分隔为执行级别。这使得其他程序员更容易解释。
事实上,即使结果很明显并且遵循标准运算符优先级,使用括号将事物分解为明确定义的层也更容易理解你期望的结果。在某些情况下,即使是编译器也无法正确解释您要做的事情。想看一个例子吗?那么,看看下面的代码。这是一个结果完全不可预测的代码的例子,因为即使是编译器也很难理解应该做什么。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print("Factoring: { ", #X, " } is: ", X) 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. char v1 = 9, 09. v2 = 5; 10. 11. PrintX(v1); 12. PrintX(v2); 13. PrintX(v1++ & 1 * v2 << 1); 14. PrintX(v1); 15. PrintX(v2); 16. } 17. //+------------------------------------------------------------------+
代码 04
在这种情况下,当您尝试编译代码 04 时,编译器将发出如下警告:
图 05
请注意,尽管有警告,编译器仍会生成代码。然而,这里存在一个重大风险,即代码在某些情况下可能不会产生正确的结果。因此,盲目信任此代码是不明智的,尤其是当编译器发出警告,指出表达式中存在潜在缺陷时。在这种情况下,有必要使用括号。如果不添加它们,只使用运算符优先级规则,我们可以看到在这种特定情况下结果是否仍然正确。
为此,我们运行代码,结果如下:
图 06
这里,输出的是值 8。但这个值真的正确吗?为了确定这一点,我们需要手动分解代码如何处理表达式。这种分析在编程中很常见。与一些人的想法相反,程序员不会在不知道结果应该是什么的情况下简单地编写代码。优秀的程序员总是知道他们的代码应该产生什么结果。在没有首先了解预期结果的情况下,切勿写任何东西。事实上,一个好的程序员基本上会对自己的逻辑进行回溯测试,然后进行所谓的正向测试,逐一审查每个结果。只有在一轮彻底的测试之后,他们才开始信任他们的代码,尽管从来没有完全信任过。一个好的开发人员总是保持健康的怀疑态度。但另一篇文章将探讨维持这种怀疑态度背后的原因。现在,让我们检查一下代码 04 的结果 8 是否正确。
为了验证这一点,我们需要了解编译器如何解释它被要求计算的内容。由于编译器遵循严格的优先级规则,这些规则在运算符优先级表中列出,我们可以在第13行手动对计算进行因子分析,看看它是否正确。
我们首先确定优先级最高的运算符。在这个例子中,它是应用于变量 v1 的 ++ 运算符。但是,此运算符是从右向左计算的。因此,在这种情况下,它的优先级将被改变,其效果将在其右侧的所有更高优先级操作完成后应用于变量 v1。在我们的例子中,这样的运算符是乘法运算符,其优先级高于左移运算符和and运算符。因此,第一个运算是将 1 乘以 v2,结果为 5。接下来,应用左移位运算符将值 5 向左移位一位。这将生成一个等于 10 的新值。然后我们对 v1 和 10 进行按位与运算。由于 v1 为九,因此应用与运算后的结果为 8。最后,右边的操作全部执行完后,v1 的值加 1。因此,当我们分解因数时,我们得到的值是 8,而当我们完成分解因数时,我们得到的 v1 的值是 10。
您现在可能会想:“啊,我明白了。这实际上非常简单和清楚。”但亲爱的读者,事实真是如此吗?您确定您真正理解了这个过程是如何运作的吗?让我们来一探究竟。尝试在下面的下一段代码中求解表达式怎么样?
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print("Factoring: { ", #X, " } is: ", X) 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. char v1 = 9, 09. v2 = 5; 10. 11. PrintX(v1); 12. PrintX(v2); 13. PrintX(++v1 & 1 * v2 << 1); 14. PrintX(v1); 15. PrintX(v2); 16. } 17. //+------------------------------------------------------------------+
代码 05
毫不犹豫地回答这个问题。如果你能真正正确地回答它,那么你真的理解优先级是如何工作的。但即便如此,只是为了稍微戏弄一下你,我还是会向你展示一个结果,并希望你告诉我它是对还是错。
当我运行这段代码时,我得到了如下所示的结果:
图 07
现在告诉我:为什么我的结果是 10 而不是 8?这是有原因的,它属于优先级规则。好吧,亲爱的读者,既然我不太喜欢和人打交道,让我们深入探讨一下为什么这些结果不同。但为此,我们需要一个新的主题。我不想仅凭一篇文章就让任何人的大脑陷入混乱。我们还有一个更重要的主题要讨论。
回测和前测
亲爱的读者,你需要理解的第一件事是:在编写任何代码之前,你应该已经知道它会产生什么结果。编程不是为了得到未知的答案而编写代码。恰恰相反,您编写代码来产生一个您已经知道的结果。而在本例中,无论是代码 04 还是代码 05,我都已经知道结果应该是什么了。即使知道由于编译器的警告,它可能是不正确的,我仍然有一个预测。在编写代码之前,我如何知道结果?这似乎毫无意义。好吧,实际上,很多人告诉你先编码,然后再看结果。但是一旦你理解了优先级,你甚至可以在打印之前就知道结果。
为了清楚地说明这一点,让我们做以下事情:如附件所示,我将提供这些代码示例,这样你就可以自己运行它们,并确切地看到我在这里显示的内容。这样,您将真正理解为什么提前知道答案如此重要,甚至在编写给您的代码之前。
现在,真正的问题来了。哪个答案是正确的:图 06 中的答案还是图 07 中的答案?如果我告诉你两者都是正确的呢?你会说什么?或许我已经失去理智了。毕竟,同一个表达式怎么能产生两个不同的结果,而且都是正确的呢?尽管听起来很疯狂,但这两个答案都是正确的。然而,您期望的结果更有可能是图 07 所示的结果。同时,图 06 的结果是对优先级的误用或误解造成的副作用。如果我们在代码 04 中使用括号明确定义运算符顺序,则结果将与图 07 中看到的结果相匹配。而且它不需要太多:只需要一个小的更改,就可以使增量运算符(++)以更高的优先级执行。在代码 05 中,由于增量运算符位于变量之前,因此它优先于乘法。但如前所述,在代码 04 中,运算符位于变量之后,这意味着它仅在表达式的其余部分之后执行。
不相信我?那我们测试一下怎么样?我们将代码 04 的第 13 行修改为如下所示的代码行:
PrintX((v1++ & 1) * (v2 << 1));
通过此更改,编译器将不再警告结果可能不可靠。然而,现在相同的代码 04 已经经过了这个简单的修改,它将给我们带来图 07 所示的结果。这正是编程的某些方面看起来如此复杂的原因。许多人学习编程是出于错误的原因。实际上,编程的真正目的是让计算机更快地给我们一个已知的结果。当我们试图找到一个我们还不知道的答案时,我们可以将计算机的答案视为有效的线索。但我们对此的信任程度是有限的。不过,在这些限制范围内,一旦你的代码经过测试,无论是使用已知值还是可以手动验证的值,你最终都可以说:“我的程序可以计算这个或那个”。在那之前,你的代码可能有错误,因为它没有经过适当的测试。
好吧,我会让你自己花时间研究这个问题,并反思我们所涵盖的内容。但在我们结束这篇文章之前,我想谈最后一件事。实际上,还有一个我们还没有研究过的运算符。让我们继续讨论最后一个主题。
三元运算符
这是我最喜欢的运算符。任何关注我的文章和社区的人可能都厌倦了看到我使用它的频率。为什么它是我的最爱?很简单:它允许我在不需要 IF 语句的情况下构建逻辑。IF 是编程的基本部分,因为它控制执行流。但在许多情况下,我们可以在不能使用 IF 的情况下使用三元运算符。在我看来,三元运算符是一个中级工具。在真正充分利用它之前,你需要对其他运算符有一个扎实的了解。除此之外,您还需要很好地理解流控制在三元结构中的行为。它很少被单独使用。大多数时候,它与赋值运算符或一些逻辑运算符配对。这就是为什么我现在只解释如何阅读它。我会尽量不使用它,至少现在还不行。
让我们来看一个可以应用三元运算符的简单代码示例。当然,还有其他方式可以写出来。但这里的目标纯粹是教学性的。所以不要担心是否可以使用其他方法。只需关注概念即可。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define PrintX(X) Print("Factoring: { ", #X, " } is: ", X) 05. //+------------------------------------------------------------------+ 06. void OnStart(void) 07. { 08. char v1 = 9, 09. v2 = 5; 10. 11. PrintX(v1); 12. PrintX(v2); 13. PrintX(v1 * ((v2 & 1) == true ? v2 - 1 : v2 + 1)); 14. PrintX(v2++); 15. PrintX(v1 * ((v2 & 1) == true ? v2 - 1 : v2 + 1)); 16. PrintX(v1); 17. PrintX(v2); 18. } 19. //+------------------------------------------------------------------+
代码 06
当执行代码 06 时,您将看到以下结果:
图 08
因为三元运算符可能会让很多人感到困惑,尤其是初学者。我会仔细解释这里发生了什么。不过,你应该花时间彻底研究整篇文章,以便真正理解这一点。慢慢读一遍,密切注意我想解释的内容。这个话题很深奥,可能需要花一些时间才能完全理解。
让我们回到代码 06。大部分内容都很容易理解。当然,我们使用宏,我还没有解释如何将其集成到您自己的程序中。然而,即使是这个宏也相对容易理解。由于本文中的其他示例中也使用了它,因此这里的解释也适用于这些示例。第 4 行定义的宏允许我们根据代码中使用的内容向终端发送信息。但具体怎么做呢?为了做到这一点,我们向它传递一个参数。当该参数前面有井号(#)时,编译器将被指示采用该参数并按其写入方式准确显示它。因此,我们可以显示正在执行的计算,以及结果值,在某些情况下,还可以显示所用变量的名称。这是调试某些类型代码的好方法。
但这只是故事的一部分。这里真正重要的是第 13 行和第 15 行是如何工作的,因为这两行都使用了三元运算符。为了以简单的方式解释这一点,让我们只关注其中一行作为我们的参考,因为另一行的行为方式大致相同。
所以,让我们取第 13 行,用不使用三元运算符的方式重写它,而是使用 IF 语句。但是为什么要使用 IF 语句来解释三元运算符呢?因为三元运算符本质上是一个压缩的 IF,具有作为表达式的额外好处,这意味着它可以放置在变量或值通常会去的地方。然而,尽管有这种相似性,三元运算符并不能取代 IF 语句。三元运算符的结构中不能包含完整的代码块,这与 IF 不同,IF 允许代码块,但不能用作返回值的表达式。
好了。因此,使用 IF 语句将第 13 行转换为等效形式,我们将得到如下内容:
if ((v2 & 1) == true) value = v1 * (v2 - 1); else value = v1 * (v2 + 1); Print("Factoring: { v1 * ((v2 & 1) == true ? v2 - 1 : v2 + 1) } is: ", value);
这里唯一要注意的细节是变量值,从技术上讲它并不存在。我只是用它来说明编译器是如何解释的。因此,将 value 视为您无法直接访问的临时变量。
有了这个概念,就更容易理解编译器如何解释三元运算符。请注意,使用 IF 语句时,代码的可读性会大大提高。当然,在真正需要三元运算符的情况下,这是不切实际的。但就教学目的而言,它是有效的。这同样适用于这个小翻译片段中的 Print 命令。此命令表示宏将在实际代码中执行的操作。
乍一看,这类事情可能看起来相当复杂。特别是因为,正如我已经提到的,这种类型的编码代表了我认为的中级概念。所以,亲爱的读者,不要着急。慢慢地学习和练习。但无论如何,一旦你理解了三元运算符本质上是一个专门的 IF,我相信随着我们进入未来的文章,一切都会变得更容易理解。
最后的探讨
在这篇文章中,我介绍并试图解释编程中最复杂的主题之一,至少从理论角度来看是这样。在实践中,运算符的主题要简单得多,学习起来也更直观。这是因为,一旦你看到每个操作或实现的结果,就更容易理解程序员并没有试图创建未知的东西。每个程序都是为了回答一个我们已经知道答案的问题而设计的。然而,随着应用程序通过测试和改进而发展,我们最终开始使用它来为之前解决的特定问题提供更快的答案。
总而言之,我给大家留下以下建议:学习并针对不同类型的挑战进行实践。只有通过持续的练习,你才能真正掌握每个操作员应该如何使用。不要想当然地认为仅仅拥有理论知识就足够了,因为事实并非如此。当操作运算符时,经验比理论更重要。所以开始练习吧。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15440
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



