Работа над ошибками и отладка

Искусство программирования базируется не только на умении вложить в программу, что и как она должна делать, но и на том, чтобы оградить её от потенциальной возможности сделать что-то не так. Второе, к сожалению, гораздо труднее выполнить из-за множества не столько очевидных заранее факторов, влияющих на поведение программы: некорректные данные, недостаток ресурсов, чужие и собственные ошибки кодирования — вот лишь некоторые из проблем.

При написании программ никто не застрахован от ошибок. Ошибки могут проявляться на разных этапах и условно делятся на:

  • Ошибки компиляции, которые выдает компилятор при обнаружении исходного кода, не отвечающего требуемому синтаксису (с такими ошибками мы уже познакомились ранее); их проще всего исправлять, поскольку их поиск выполняет компилятор;
  • Ошибки времени исполнения программы, которые выдает терминал при возникновении в программе некорректного условия, такого как деление на ноль, взятие квадратного корня из отрицательного числа или попытка обратиться к несуществующему элементу массива, как случилось в нашем случае; их сложнее обнаружить, потому что они, как правило, возникают не при любых значениях входных параметров, а только при конкретных специфических условиях;
  • Ошибки проектирования программы, которые приводят к её полной неработоспособности без каких-либо подсказок со стороны терминала, например, зависание в бесконечном цикле; такие ошибки могут оказаться самыми сложными в плане их локализации и воспроизведения, а ведь возможность воспроизвести проблемное состояние программы — необходимое условие для последующего исправления;
  • Скрытые ошибки, когда программа вроде бы работает гладко, но выдаваемый результат не соответствует правильному — это легко обнаружить, если 2*2 не равно 4, а на практике заметить расхождения бывает намного сложнее.

Но вернемся к разбору конкретной ситуации со скриптом. Согласно сообщению об ошибке, которую нам выдала среда исполнения MQL-программ, неверно написана следующая инструкция:

return messages[hour / 8]

При вычислении индекса элемента из массива, в зависимости от значения переменной hour, может быть получена величина, выходящая за размер массива, равный трем.

Убедиться в том, что именно так и происходит, позволяет встроенный в MetaEditor отладчик. Все его команды собраны в меню Отладка. Они предоставляют много полезных функций, но мы здесь остановимся только на двух: Отладка -> Начать на реальных данных (F5) и Отладка -> Начать на исторических данных (Ctrl+F5). Про остальные можно почитать в Справке по MetaEditor.

Обе команды компилируют программу особым образом — с отладочной информацией. Такая версия программы получается не оптимизированной, как при стандартной компиляции (подробнее про оптимизацию см. Документацию), зато позволяет за счет отладочной информации "заглянуть" внутрь программы в процессе выполнения: увидеть состояние переменных и стек вызовов функций.

Разница между отладкой на реальных данных и исторических заключается в том, что в первом случае программа запускается на онлайн-графике, а во втором — на графике тестера в визуальном режиме. Для указания редактору, какой именно график и с какими настройками использовать (символ, таймфрейм, диапазон дат и прочее), следует предварительно открыть диалог Настройки -> Отладка, и заполнить в нем требуемые поля. Опция Использовать указанные настройки должна быть включена. Если данный флаг сброшен, при онлайн-отладке будет использован первый символ из Обзора рынка и таймфрейм H1, а при отладке на истории берутся настройки тестера.

Обратите внимание, что в тестере можно отлаживать только индикаторы и эксперты. Для скриптов доступна только отладка онлайн.

Запустим наш скрипт с помощью F5 и введем 100 в параметр GreetingHour, чтобы воспроизвести прошлую проблемную ситуацию. Скрипт начнет выполняться и почти сразу терминал выведет сообщение об ошибке и запрос на открытие отладчика.

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, что соответствует максимальной кратной 5 величине в составе 11, а это 10. А остаток между 11 и 10 как раз и дает 1.

Для исправления ошибки в функции Greeting достаточно предварительно выполнить деление hour по модулю на 24 — таким образом будет обеспечено, что номер часа окажется в диапазоне 0 — 23. Функция Greeting станет выглядеть следующим образом:

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

Хотя данная правка, несомненно, сработает хорошо (мы проверим это через минуту), она не затрагивает другую проблему, которая осталась вне нашего внимания. Дело в том, что параметр GreetingHour имеет тип int, то есть может принимать не только положительные, но и отрицательные значения. Если бы мы попытались ввести, например, -‌8 (или большее отрицательное число), то получили бы ту же ошибку времени выполнения — выход за пределы массива, только на этот раз индекс превышает не максимальное значение (размер массива), а становится меньше минимального (в частности, -8 приводит к обращению к -1-му элементу, причем, что интересно, значения от -7 до -1 отображаются на 0-й элемент и ошибки не вызывают).

Для исправления данной проблемы мы заменим тип параметра GreetingHour на тип беззнакового целого: вместо int будем использовать uint (про все доступные типы мы расскажем во второй части, а здесь нам пригодится именно uint). Руководствуясь ограничением на неотрицательность значений, встроенном на уровне компилятора для uint, MQL5 самостоятельно обеспечит, чтобы ни пользователь (в диалоге свойств), ни программа (в своих расчетах) не "уходили в минус".

Сохраним новую версию скрипта под именем GoodTime2, откомпилируем и запустим. Введем значение 100 для параметра GreetingHour и убедимся, что на этот раз скрипт выполняется без ошибок, а в журнал терминала выводится приветствие "Good morning". Это ожидаемое (правильное) поведение, так как мы можем с помощью калькулятора проверить, что остаток от деления 100 на 24 по модулю дает 4, а деление 4 на 8 без остатка равно 0 (а это обозначает у нас утро). Конечно, такое поведение можно посчитать неожиданным с точки зрения пользователя, но с другой стороны, он тоже действовал неожиданно, вводя в качестве номера часа значение 100. Может быть он полагал, что наша программа упадет? Но этого не произошло, и это положительный момент. Разумеется, в реальных программах нужно проверять вводимые значения на допустимость и сообщать пользователю о нестыковках.

Здесь же в качестве дополнительной меры по предупреждению ввода неверного числа воспользуемся специальной возможностью MQL5 указать для входного параметра более подробное "дружелюбное" название. Достигается это с помощью комментария, указываемого после описания входного параметра, в той же строке. Например, так:

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

Обратите внимание, что в комментарии мы написали слова из имени переменной раздельно (потому что это уже не идентификатор в коде, а подсказка о нем для пользователя). Кроме того, мы добавили в круглых скобках диапазон допустимых значений. При запуске такого скрипта в диалоге ввода параметров прежний GreetingHour будет показан как:

Greeting Hour (0-23)

Теперь можно быть уверенным, что если кто-то и введет 100 в качестве часа, это не наша вина.

Внимательный читатель может поинтересоваться, зачем мы определили функцию Greeting с параметром hour и передаем в неё GreetingHour, если можно было использовать входной параметр непосредственно в ней. Функция, как обособленный логический фрагмент кода, формируется не только для дробления программы на обозримые, простые для понимания части, но и для последующего повторного использования. Функции, как правило, вызываются из нескольких частей программы или входят в состав библиотеки, которую подключают ко многим разным программам. Поэтому правильно написанная функция должна быть независимой от внешнего контекста и переносима между программами.

Например, если потребуется перенести нашу функцию Greeting в другой скрипт, то там она перестанет компилироваться, так как в нём не будет параметра GreetingHour. И требовать добавить его не совсем корректно, потому что другой скрипт может рассчитывать время каким-то другим способом. Иными словами, при написании функции нужно стараться избавиться от необязательных внешних зависимостей. Вместо них следует декларировать параметры функции, которые могут быть заполнены вызывающим кодом.