
从基础到中级:WHILE 和 DO WHILE 语句
概述
此处提供的材料仅用于教学目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用该应用程序。
在上一篇文章,从基础到中级:IF ELSE 语句中,我解释了如何检查代码来实现条件流控制。这样做是为了根据所分析的表达式,我们可以选择是执行一个还是另一个过程。
尽管 IF 语句及其盟友 ELSE 非常有用,它允许我们实现几乎任何代码,但在实践中这并不是很明智。只使用两个语句会使代码的某些部分很难理解和消化。
正是出于这个原因,编程语言并不完全依赖于 IF-ELSE 对。为了使代码更易读、更容易理解,它们包含了更广泛的指令集,包括循环语句。
一般来说,毫不夸张地说,循环语句是存在的最危险的控制流指令之一。这是因为在实现循环时,即使是一个微小的编程错误也可能导致灾难。这是一个严重的问题,因为你的代码将进入一个无限循环。许多初学者在遇到这样的问题后放弃了。他们只是对在代码中实现循环的前景感到恐惧。
在程序中创建循环基本上有两种方法。第一种涉及在实现循环的主例程之外使用函数和过程。第二种是通过控制流语句。
由于使用控制流语句是更简单的方法,我们现在将从它开始。然而,随着我们的进展,我将演示如何使用函数和过程创建循环。与许多人可能认为的相反,使用函数或过程来创建循环而不是仅仅依赖于控制流语句是有充分理由的。但在更合适的时候,解释这一点将是另一篇文章的主题。
阅读本文的前提是:了解变量和常量。我们已经讨论过这个话题。然而,除了知道变量和常量是什么之外,这里最关键的方面是了解位宽如何影响值以及变量的类型如何影响其值。
如果你不了解我的意思,请查看从基础到中级:操作符,我在这里解释了一些与该主题相关的关键概念。虽然那里的内容是基础的,但它应该足以让你跟随这里将要讨论的内容。我强烈建议您探索其他文章,以便更全面地了解本文中介绍的所有内容。
此时,我面临着一个小困境。虽然 FOR 语句在创建循环时通常更简单,并且通常是程序员的最爱,但它也有一些缺点。另一方面,用于创建循环的替代控制语句往往比 FOR 语句风险更大。所以,我正在考虑先介绍哪一个。与此同时,让我们按照以下步骤进行:我们将开始一个新的部分,在那里我将解释一些与这个主题相关的重要概念。
为什么要使用循环?
许多程序员都非常害怕在代码中创建循环。其他人尽可能避免使用它们,只有在没有其他方法的情况下才使用它们。但是为什么人们对使用循环如此恐惧呢?原因很简单:循环本身就涉及风险。每次进入一个循环时,您都会依赖于代码的执行,需要特定条件才能在需要时正确终止循环。否则,一个循环可能会在你没有意识到的情况下进入一个无限循环,导致你错误地认为计算机只是花了太长时间来处理一些东西,而事实上,代码已经陷入了一个无限的循环中。
许多开发人员担心循环的另一个原因是,分析循环中发生的事情可能非常困难。这是因为,在现实世界的应用程序中,循环在完成之前通常需要执行数千甚至数百万次。根据所执行的计算类型,这可能需要几个小时才能得出最终结果。
我知道这对许多人来说似乎是不可想象的。一个程序怎么可能需要几个小时才能完成一项任务?然而,它确实发生了。在某些情况下,完成一项任务确实需要数小时的处理时间。也就是说,在大多数情况下,特别是在教学环境中,您编写的循环相对较小,规模为数千次迭代。通常,这会导致执行时间仅为几秒钟或几分钟。一个很好的例子是为训练神经网络而设计的应用程序。
通过我的其他文章,我演示了如何实现这样的应用程序:完全用纯 MQL5 编写的神经网络。在我看来,这是一个中级项目,因为它只需要对 MQL5 和底层数学有扎实的理解。其余部分相对简单。
现在,我还没有回答本节标题中提出的问题:为什么要使用循环?答案在于它们为您的代码带来的简单性。循环允许您控制执行特定操作的次数,而无需手动重复代码或依赖 Ctrl+C 和 Ctrl+V。考虑以下示例。早些时候,我介绍了一段简单的代码,用于计算给定数字的阶乘。您可能还记得,代码本身非常简单。对于那些还没有看过的人,这里再说一遍:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. uchar counter = 0; 07. ulong value = 1; 08. 09. Print("Factorial of ", counter, " => ", value); 10. counter = counter + 1; 11. value = value * counter; 12. Print("Factorial of ", counter, " => ", value); 13. counter = counter + 1; 14. value = value * counter; 15. Print("Factorial of ", counter, " => ", value); 16. counter = counter + 1; 17. value = value * counter; 18. Print("Factorial of ", counter, " => ", value); 19. counter = counter + 1; 20. value = value * counter; 21. Print("Factorial of ", counter, " => ", value); 22. } 23. //+------------------------------------------------------------------+
代码 01
对于较小的数字,代码 01 可能是可以接受的。然而,随着数字的增加,事情开始变得更加复杂。即使对于 20 的阶乘,设置也会非常困难,此外代码实现中出现错误的可能性很大,即使使用 CTRL+C 和 CTRL+V 也是如此。因此,在出现此代码 01 的同一篇文章中,我们开发了另一个代码,如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Factorial(); 07. Factorial(); 08. Factorial(); 09. Factorial(); 10. Factorial(); 11. } 12. //+------------------------------------------------------------------+ 13. void Factorial(void) 14. { 15. static uchar counter = 0; 16. static ulong value = 1; 17. 18. Print("Factorial of ", counter, " => ", value); 19. counter = counter + 1; 20. value = value * counter; 21. } 22. //+------------------------------------------------------------------+
代码 02
此时,您可能会注意到我们的代码变得更加紧凑,使得使用 Ctrl+C 和 Ctrl+V 进行小规模因式分解稍微容易一些。但是,如果你需要分解一个大得多的数字呢?你怎么能这样做呢?好吧,假设你没有超过 ulong 值的 64 位上限,你就必须多次复制和粘贴阶乘层。当然,这并不包括每次需要计算新的阶乘时调整代码所需的所有额外工作。显然,这会更加乏味而不是有趣。因此,使用循环使代码更易于管理。这也解释了为什么循环至关重要。
现在我们已经建立了这种理解,我们可以开始讨论要使用的语句。虽然 FOR 循环是许多程序员(包括写这篇文章的程序员)的最爱,但让我们从另一个语句开始。这种替代方案有两种可能的实现,但与 FOR 循环相比,它更容易解释,因此也更容易掌握,FOR 循环有某些细微差别,这促使我开始采用另一种方法。
WHILE 和 DO WHILE 语句
尽管本节标题中的措辞很俏皮,但这个循环很容易理解,因为它遵循与 IF 语句相同的原则。然而,有一些重要的预防措施需要牢记。开发人员在使用此循环时遇到困难并不罕见。原因很简单:健忘。许多程序员忘记调整或纠正决定循环何时终止的条件。令人惊讶的是,即使是经验丰富的开发人员也会犯这个错误。因此,请将此视为一个警告:在实现使用此语句的代码时要小心。
WHILE 和 DO-WHILE 对之间的区别非常简单。WHILE 可能根本不执行内部例程,而 DO-WHILE 保证例程至少运行一次。基本上就是这样。很简单,不是吗?然而,这并没有降低所涉及的风险。那么,让我们从两者中更简单的一个开始:WHILE 语句。其执行流程如下图所示。
图 01
请注意,它与前一篇文章中讨论的 IF 语句非常相似。事实上,表达式在这里的作用方式遵循相同的原则。这意味着只有当表达式的计算结果为 true 时,循环才会执行定义的例程。换句话说,表达式必须为非零,循环才能运行。这就是我首先介绍 IF 语句的原因。理解这句话对于任何有志于成为一名熟练程序员的人,甚至对于那些想写代码只是为了好玩的人来说都是至关重要的。
很好,由于这种结构与 IF 语句非常相似 — 亲爱的读者,我希望你已经通过研究它做了功课 — 我们现在可以更直接地采用我们的方法。
让我们从一段非常简单的代码开始,为您提供循环的基本介绍。我不想让你绊倒,但我想帮助你理解它们在实践中是如何运作的。为此,让我们分析以下代码:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char info = 10; 07. 08. Print(__FUNCTION__, " ", __LINE__, " It will be executed anyway..."); 09. 10. while (info) 11. { 12. info = info - 1; 13. Print(__FUNCTION__, " : ", info); 14. } 15. 16. Print(__FUNCTION__, " ", __LINE__, " It will be executed anyway..."); 17. } 18. //+------------------------------------------------------------------+
代码 03
此代码 03 有效地说明了 WHILE 循环如何运行。执行时,它将产生如下所示的输出。
图 02
图 02 中突出显示的部分表示循环生成的输出部分。这是在 WHILE 循环中执行的例程。现在,让我们快速看看这是如何发生的,以及为什么倒计时从 9 开始,到 0 结束。
要理解这一点,您需要观察两个关键细节。首先,在第六行中,我们用值十初始化变量。当 WHILE 语句计算条件(即第六行中设置的变量的值)时,它确定条件为 true,从而启动循环。但是,这是第二个重点,在第 13 行将值打印到终端之前,我们在第 12 行将变量信息减一。这就是为什么倒计时从 9 到 0。循环在零处终止,因为零被视为布尔 false 值。
但是,如果我们不递减第 12 行中的 info 变量,而是将其递增 1,会发生什么?循环会进入无限循环吗?亲爱的读者,这是一个很好的问题,你可以自己尝试一下,看看结果。然而,在测试之前,让我们先了解为什么在这种特殊情况下循环不会进入无限循环。
与普遍的直觉相反,数字从负无穷大扩展到正无穷大,计算中的工作方式不同。计算机可以表示的每个值都有上限和下限。该限制由所使用的变量类型的位宽决定。我们在之前的文章中讨论过这个问题。由于这种限制(请记住,有些语言没有施加这样的约束,尽管这超出了我们的范围),如果值更改确保变量最终达到零,循环将在某个时候终止。无论需要多长时间,它最终都会结束。然而,在这种情况下,WHILE 循环存在一些具体的挑战。为了避免现阶段的混淆,我们将把这个讨论留到另一个时间。最有可能的是,在下一篇文章中。
所以,这基本上就是关于 WHILE 语句的全部内容。然而,现在还有一个额外的细节可能需要解释。当我们希望代码以受控的方式运行时,特别是在循环方面,除了通常的终止条件外,我们通常还会包含一个次要的退出条件。这起到了故障保护的作用,允许我们强制循环结束,以便程序可以继续正常执行。
由于 MQL5 是一种事件驱动语言(我们稍后将更深入地探讨这一主题),因此创建无限期等待事件的循环并不常见。这种行为在 C 或 C++ 等语言中更为常见,这些语言本身不是事件驱动的。
在实践中,我们经常使用循环在应用程序中创建受控暂停。此暂停在预定的时间段内保持活动状态,或者直到特定事件发生。然而,直接为此目的使用 WHILE 循环并不常见。原因是 WHILE 循环只有在初始条件为真时才会执行。在许多情况下,这并不是我们所需要的。
也就是说,在需要紧急出口的情况下,WHILE 循环通常是更好的选择。如果条件从一开始就评估为 false,则循环根本不会执行。这一点至关重要,因为在许多情况下,循环的例程可能会根据某些标准直接影响它是否应该继续运行。在讨论 DO-WHILE 循环时,我们将进一步探讨这一概念。
现在,让我们执行以下操作。我们将有意创建一个理论上可以是无限的循环。然而,我们还将实现允许其终止的条件。这种方法如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char info = 5; 07. 08. Print("Press ESC to finish counting..."); 09. 10. while (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) 11. { 12. info = info - 1; 13. Sleep(200); 14. Print(__FUNCTION__, " : ", info); 15. } 16. 17. Print(__FUNCTION__, " ", __LINE__, " It will be executed anyway..."); 18. } 19. //+------------------------------------------------------------------+
代码 04
在代码 04 中,第 10 行有一个循环,理论上,它会无限执行。但是,如果用户按下 ESC 键,则不会发生这种情况。需要注意的一个重要细节是,在循环开始之前,警报消息只显示一次。如果用户错过了打印到终端的这条消息,可能会导致一些混乱。在终端中,您将看到与下图所示类似的输出。
图 03
请注意,这一次,'info' 变量的值变为零,但循环仍在继续。这是因为决定循环何时终止的条件现在取决于是否按下 ESC 键。换句话说,程序在允许执行继续之前等待事件。MQL5 脚本中使用的另一个常见测试是检查脚本是否已中断。要实现这一点,只需将代码 04 中的第 10 行替换为下面这一行。
while ((!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) && (!IsStopped()))
这个小修改允许代码 04 中的循环以两种方式终止:按 ESC 键和手动从图表中删除脚本。很简单,不是吗,亲爱的读者?没有理由害怕循环。然而,我们仍然有一个问题:在执行过程中很容易错过第 8 行中的警报消息。这意味着用户可能没有意识到按 ESC 键也会停止应用程序。
解决这个问题的一种方法是将第 8 行放入循环中。但是,我们不需要直接修改 WHILE 循环,而是借此机会探索 DO-WHILE 循环变体。为了使演示更加清晰,我们将在一个新的主题中探讨这一点。
DO...WHILE 循环
我忍不住在这个章节标题中加入了一点幽默。但在某种程度上,它完美地描述了 DO-WHILE 循环。虽然它是一个单独的语句,但其功能更像是一对语句,就像 IF ELSE 一样。然而,这里的情况略有不同。
从我们之前的讨论中,您可能还记得 WHILE 循环只有在其条件为真时才会执行。这里的关键区别在于,DO-WHILE 循环至少执行一次其例程,而不管条件最初是真是假。这是因为语句 DO(字面意思是“ 做 ”)位于 WHILE 条件(意思是“ 到某时 ”)之前。这确保在检查条件之前循环内的例程至少运行一次。这种行为在某些情况下特别有用,使我们能够更好地控制特定例程的功能,特别是当我们需要在评估条件之前至少执行一次时。
为了说明这一点,让我们以一种确保警报消息定期出现的方式修改代码 04。虽然我们可以在代码 04 中直接实现这一点,但让我们假设复制第 8 行是不可行的。更重要的是,我们希望确保循环至少执行一次,而不管任何先前条件如何。
因此,我们得到如下代码:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char info = 10; 07. 08. do 09. { 10. if (((info / 5) * 5) == info) Print("Press ESC to finish counting..."); 11. info = info - 1; 12. Sleep(200); 13. Print(__FUNCTION__, " : ", info); 14. }while (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)); 15. 16. Print(__FUNCTION__, " ", __LINE__, " It will be executed anyway..."); 17. } 18. //+------------------------------------------------------------------+
代码 05
当你运行这段代码时,你会看到类似于下面所示的内容。
图 04
在这里,我们正在处理一些在某种意义上非常有趣的事情。这是因为代码 05 中的某些操作乍一看似乎不合逻辑。但如果你一直认真关注和研究以前发表的文章,你就能相对较快地理解它们。
好吧,让我们来弄清楚这里发生了什么,以及为什么现在会不时出现要求您按 ESC 的消息。如您所见,现在检查是否继续执行循环内的过程在第 14 行完成,并且完整的循环过程至少执行一次。希望现在已经清楚了为什么 DO 元素与 WHILE 元素以这种组合共存。
现在,请密切关注我即将解释的内容,因为它非常重要。这里的想法与 WHILE 语句相同。循环将一直执行,直到 WHILE 语句表达式变为 false。但是你怎么知道循环从哪里开始呢?如果删除第 8 行,您将无法确切知道循环从哪里开始。更糟糕的是,需要循环的过程(从第 10 行到第 13 行)将不会被视为过程,而是普通代码。
第 14 行所示的 WHILE 语句确实是一个循环。然而,由于循环内不会执行其他命令,因此该过程将为空。它只会停留在那里,等待按下 ESC 键终止它。
只有基于 DO 和 WHILE 元素的组合,编译器和其他程序员才能确定循环从第八行开始到第十四行结束。这可能看起来有点令人困惑,但如果我们从第 8 行中删除 DO 元素,它会更有意义,这将使代码看起来像这样:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char info = 10; 07. 08. 09. { 10. if (((info / 5) * 5) == info) Print("Press ESC to finish counting..."); 11. info = info - 1; 12. Sleep(200); 13. Print(__FUNCTION__, " : ", info); 14. }while (!TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)); 15. 16. Print(__FUNCTION__, " ", __LINE__, " It will be executed anyway..."); 17. } 18. //+------------------------------------------------------------------+
代码 06
正如我们所看到的,由于代码中的这一小变化很容易被忽视,结果完全不同。为了使事情更清楚,我不会使用图像,而是使用下面的 gif 显示正在发生的事情。
动画 01
这就是在没有适当关注的情况下创建循环的危险之一。请注意,我们不再看到与图 04 中相同的行为。在这里,应该是循环主体的东西被执行,就像它是一个常规的代码序列一样。然而,它在这一点上停止了,代码进入了第 14 行所示的循环。因此,如果循环的终止条件取决于循环例程中的某个动作,则该动作永远不会发生。因此,一个被设计为具有退出条件的循环永远不会终止,从而导致可怕的无限循环。
现在,让我们分析一下第 10 行上的消息是如何以及为什么间隔打印的。为了理解这一点,我们需要检查第 10 行 IF 语句中的表达式。虽然这个表达式对人类来说可能没有意义,但对计算机来说是完全合乎逻辑的,尤其是在像 MQL5 这样的强类型语言中。
另一篇文章已经介绍了编程语言中的数学计算。也就是说,将变量信息的值除以一个数字,然后立即将结果乘以该数字,这种看似荒谬的操作可能看起来毫无意义。然而,如果你刚刚开始探索编程,我强烈建议你阅读前面的文章。原因是执行此计算可能会产生与 “info” 中的原始值匹配或不同的结果。如果值相同,则将打印消息。如果它们不同,则不会。
由于此代码将作为附件提供,您可以更深入地研究它。要记住的关键点是,你必须对除法和乘法使用相同的值,并且总是先除法后乘法。但要谨慎使用整数值。由于我们稍后将讨论的某些因素,浮点数将不起作用。
为了结束这个主题,让我们查看一下 DO-WHILE 循环的执行流程图。如下所示。
图 05
正如你所看到的,它并不像在实际操作之前最初看起来那么复杂。理解执行流程比简单地记忆命令和语法重要得多。一旦你掌握了执行流程的工作方式,你就能够思考得更清楚。通过持续的练习,认识到流程中的每个小决定是如何影响最终结果的。随着时间的推移,即使作为一名业余爱好者,你也会自然地发展编程技能。你将变得足够熟练,可以学习其他编程语言,而不局限于一种。
最后的想法
在这篇文章中,我的目的是以最容易理解和最直接的方式介绍这个主题。许多初级程序员将循环视为一场噩梦。然而,我希望已经证明,只要有耐心、专注和细心,无论你是自己实现代码还是研究其他程序员的工作,你都可以完成一些真正有趣的事情。这些都是非常有用的东西。
如果正确有效地使用,循环可以节省大量时间,不仅在结构化条件方面,而且在简化逻辑方面。事实上,本文中的早期示例,我演示了没有循环的数字因式分解,只使用顺序调用,可以以更紧凑、更高效的方式重写。下图展示了循环如何实现优雅实用的解决方案。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. uchar counter = 0; 07. ulong value = 1; 08. 09. while (counter < 5) 10. { 11. counter = counter + 1; 12. value = value * counter; 13. } 14. 15. Print("Factorial of ", counter, " => ", value); 16. } 17. //+------------------------------------------------------------------+
代码 07
尝试将代码 02 更改为使用循环,而不是第 6 行和第 10 行之间的那些连续调用。如果你找不到方法,别担心。我们将在下一篇文章中更多地讨论循环,因为我们还没有涵盖所有内容。所以,在为时已晚之前开始练习吧。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15375



