
从基础到中级:运算符优先级
概述
此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
在上一篇文章“从基础到中级: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



