错误修复与调试

编程的艺术就在于对程序的操控,既要明确指示程序的执行内容和方式,也要加以防范,以免出现错误行为。遗憾的是,由于影响程序行为的因素众多且难以发现,导致后者的执行难度很大。数据错误、资源不足、他人和自己的编码错误只是问题的冰山一角。

没人能保证编写的程序不会出错。错误可能发生在不同的阶段,可以简单地分为以下几种:

  • 编译错误:编译器在识别出不符合所需语法的源代码时返回(我们在上文已经了解了这种错误);这种错误最容易修复,因为编译器会搜索这类错误;
  • 程序运行时错误:终端在程序中出现不正确情况时返回(如被零除、计算负数的平方根或我们的例子中提到的试图引用数组中不存在的元素);这类错误比较难以检测,因为这类错误通常不会在所有输入参数值中出现,而只在特定条件下出现;
  • 程序设计错误:导致程序完全关闭,没有来自终端的任何提示,例如陷入无限循环;就定位和重现这些错误而言,这样的错误可能是最复杂的,而能否重现程序中某个问题是之后进行修复的必要条件;
  • 隐藏错误:程序看似正常运行,但给出的结果不正确;2*2 不等于 4 这样的错误很容易检测到,但一些细微偏差就很难发现了。

我们回到脚本的具体情况。根据 MQL 程序运行时环境提供给我们的错误消息,以下语句是错误的:

return messages[hour / 8]

在计算数组中元素的索引时,根据 hour 变量的值,可能会获得一个超过数组大小 3 的值。

嵌入在 MetaEditor 中的调试器确保这种情况确实会发生。调试器的所有命令都整合在“调试”菜单中。这些命令提供了许多有用的功能。在本文中,我们仅说明两个功能:调试 -> 从实际数据开始(F5) 和 调试 -> 从历史数据开始 (Ctrl+F5)。您可以在 MetaEditor 帮助中查看其他功能的帮助信息。

这两个命令都以一种特殊的方式编译程序:使用调试信息。这种版本的程序还没有像在标准编译中那样进行优化(有关优化的更多详细信息,请参见文档),同时,它允许使用调试信息在执行期间透视程序内部状态:查看变量和函数调用堆栈的状态。

对真实数据和历史数据进行调试的区别在于,前者在在线图表上启动程序,后者在测试程序图表上以可视模式启动程序。要命令编辑器使用什么样的图表和设置,如交易品种、时间范围、日期范围等。您应该先打开设置 -> 调试对话框并填写其中的必填字段。必须启用使用指定设置选项。如果禁用此选项,则来自市场报价的第一个交易品种和时间范围 H1 将用于在线调试,而在对历史数据调试时会使用测试程序设置。

请注意,在测试程序中只能调试指标和 EA 交易。脚本只能进行在线调试。

我们按 F5 来运行脚本,并在 GreetingHour 参数中输入 100 来重现上述情况。脚本开始执行,同时终端会立即显示一条错误消息并请求打开调试器。

Critical error while running script 'GoodTime1 (EURUSD,H1)'.
Array out of range.
Continue in debugger?

在用户确认后,我们将进入 MetaEditor,当前字符串在源代码中高亮显示,表明这里出现了错误(请注意左边字段中的绿色箭头)。

在发生错误的情况下处于调试模式的 MetaEditor

在发生错误的情况下处于调试模式的 MetaEditor

当前调用栈显示在窗口的左下方:其中列出了所有函数(按自下而上的顺序),这些函数是在代码在当前字符串处停止执行之前调用的。特别要注意,在我们的脚本中,调用了 OnStart 函数(由终端本身调用),然后又从中调用了 Greeting 函数(我们从代码调用这个函数)。窗口右下方有一个概览面板。可以在其中输入变量名,或者在表达式列中输入整个表达式,可在同一字符串的列中查看变量的值。

例如,我们可以使用上下文菜单中的添加命令,或者用鼠标双击第一个自由字符串来输入表达式 "hour / 8",并确保它等于 12。

由于调试因错误而停止,也就没有必要继续运行该程序了;因此我们可以执行调试 -> 停止命令 (Shift+F5)。

在较复杂的情况下,问题的根源并不明显,调试器支持对语句执行顺序和变量内容进行逐字符串监控。

为了解决这个问题,必须确保代码中的元素索引始终落在 0 至 2 的范围内,即符合数组大小。严格地说,我们应该编写一些额外的语句来检查输入数据的正确性(在我们的例子中,GreetingHour 只能取 0 至 23 范围内的值),在违反条件的情况下,显示一个提示或自动修复。

在这个介绍性项目中,我们只做一些简单的修正:我们将改进计算元素索引的表达式,保证结果始终在要求的范围内。为此,让我们再学习一个运算符:模数运算符,这个运算符只适用于整数。我们使用 '%' 符号来表示模数运算。模数运算的结果是被除数除以除数的整除法的余数。例如:

11 % 5 = 1

这里,用 11 除以 5 的得到整数为 2,对应 11 以内 5 的最大倍数 10。11 和 10 之间的余数正好是 1。

要修复 Greeting 函数中的错误,只需预先执行 hour 除以 24 的模数除法即可,这将确保小时数的范围在 0-23 之间。Greeting 函数如下所示:

string Greeting(int hour)
{
  string messages[3] = {"Good morning""Good afternoon""Good evening"};
  return messages[hour % 24 / 8];
}

尽管这种修正效果不错(我们会马上进行检查),但它并未涉及另一个我们暂未关注的问题。这个问题就是 GreetingHour 参数是 int 类型,也就是说,它可以是正值和负值。例如,如果我们尝试输入 -‌8 或者更小的负数,那么我们会得到相同的运行时错误,即超出数组范围;在这种情况下,索引没有超出最大值(数组大小),而是小于最小值(特别是,-8 导致引用第 -1 个元素,有趣的是,从 -7 到 -1 的值显示在第 0 个元素上,不会导致任何错误)。

为了修复这个问题,我们将用无符号整数替换 GreetingHour 参数的类型:我们将使用 uint 代替 int(我们将在第二章讲述所有可用的类型,这里我们需要 uint)。在 uint 编译器级别内置的非负值限制辅助下,MQL5 将自动确保用户(在属性对话框中)和程序(在其计算中)都不会输入负数。

让我们将新版本的脚本另存为 GoodTime2,编译并启动它。我们为 GreetingHour 参数输入值 100,并确保这次脚本执行时没有任何错误,同时在终端日志中打印问候语“Good morning”。这种行为在预料之内(也是正确的),因为我们可以使用计算器并检查模除法 100 除以 24 得到的余数是 4,而 4 除以 8 得到的整数结果是 0,在我们的例子中,这意味着早晨。从用户的角度来看,当然可以认为这种行为是非预期的。但是,输入 100 作为小时数本身就是一个非预期的用户操作。用户可能以为我们的程序会失败。但实际并没有,这就很好。当然,对于真实的程序,输入的值必须经过验证,并且必须将错误告知用户。

作为防止输入错误数字的附加措施,我们还将使用一个特殊的 MQL5 特性来为输入参数提供一个更详细、更友好的名称。为此,我们将在同一个字符串中的输入参数说明之后使用注释。例如,像这样:

input uint GreetingHour = 0// Greeting Hour (0-23)

请注意,我们已经将变量名中的文字单独写在了注释中(它不再是代码中的标识符,而是代码中给用户的提示)。此外,我们还在圆括号中添加了有效值的范围。启动脚本时,之前的 GreetingHour 将出现在对话框中用于输入参数,如下所示:

Greeting Hour (0-23)

现在我们可以肯定,如果用户输入 100 作为小时,这并不是我们的错误。

细心的读者可能会想,如果我们可以直接在函数中使用输入参数,那么为什么要用 hour 参数来定义 Greeting 函数并将 GreetingHour 发送到函数中。函数作为代码的一个离散逻辑片段,构建函数是为了将程序分成可见的、易于理解的部分,并在之后重用。函数通常从程序的几个部分调用,或者作为库的一部分调用(一个库连接到多个不同程序)。因此,正确编写的函数必须独立于外部上下文,并且可以在程序之间移动。

例如,如果我们需要将 Greeting 函数转移到另一个脚本,该函数将停止编译,因为函数中没有 GreetingHour 参数。要求添加此参数并不正确,因为其他脚本可以用另一种方式计算时间。换句话说,在编写函数时,我们应该尽量避免不必要的外部依赖项。相反,我们应该声明一些函数参数,这些参数可以用调用代码填充。