Обработка ошибок времени исполнения программы

Любая программа, составленная достаточно корректно, чтобы компилироваться без ошибок, все равно не застрахована от ошибок во время работы. Они могут возникать как по недосмотру разработчика, так и вследствие непредвиденных обстоятельств, возникших в программной среде ("пропал" Интернет, кончилась память). Но не менее вероятна и ситуация, когда ошибка возникает из-за неправильного применения программы: ведь, документацию пользователи читают в последнюю очередь. Во всех этих случаях программа должна иметь возможность проанализировать суть проблемы и адекватно её обработать.

Каждая инструкция MQL5 является потенциальным источником ошибок выполнения, и если таковые случаются, терминал сохраняет в специальную переменную _LastError описательный код. Разумеется, анализировать код следует непосредственно после каждой инструкции, поскольку потенциальные ошибки в последующих инструкциях могут перезаписать это значение на свое.

Обратите внимание, что существует ряд критических ошибок, при возникновении которых выполнение программы немедленно прерывается:

  • деление на ноль;
  • выход индекса за пределы массива;
  • использование некорректного указателя объекта;

Полный перечень кодов ошибок, и что они значат, приведен в документации.

В разделе Открытие и закрытие файлов мы уже обращались к проблеме диагностики ошибок в рамках написания полезного макроса PRTF. Там, в частности, был введен вспомогательный заголовочный файл MQL5/Include/MQL5Book/MqlError.mqh, в котором перечисление MQL_ERROR позволяет простым способом преобразовать цифровой код ошибки в её имя с помощью EnumToString.

enum MQL_ERROR
{
   SUCCESS = 0,
   INTERNAL_ERROR = 4001,
   WRONG_INTERNAL_PARAMETER = 4002,
   INVALID_PARAMETER = 4003,
   NOT_ENOUGH_MEMORY = 4004,
   ...
   // начало области для ошибок, определенных программистом (см. след. раздел)
   USER_ERROR_FIRST = 65536,
};
#define E2S(XEnumToString((MQL_ERROR)(X))

Здесь в качестве параметра X макроса E2S как раз должна выступать переменная _LastError или эквивалентная ей функция GetLastError.

int GetLastError() ≡ int _LastError

Функция возвращает код последней ошибки, произошедшей в инструкциях MQL-программы. Изначально, в отсутствие ошибок, значение равно 0. Разница между чтением _LastError и вызовом функции GetLastError — чисто синтаксическая (выбирайте подходящий вариант в соответствие с предпочтительным стилем).

Следует иметь в виду, что штатное безошибочное выполнение инструкций не сбрасывает код ошибки. Обращение к GetLastError также этого не делает.

Таким образом, если есть последовательность действий, из которых лишь одно выставит признак ошибки, этот признак будет возвращаться функцией и для последующих (успешных) действий. Например,

// _LastError = 0 по умолчанию
действие1// ok, _LastError не меняется
действие2// ошибка, _LastError = X
действие3// ok, _LastError не меняется, то есть по-прежнему равен X  
действие4// другая ошибка, _LastError = Y  
действие5// ok, _LastError не меняется, то есть по-прежнему равен Y
действие6// ok, _LastError не меняется, то есть по-прежнему равен Y

Такое поведение затруднило бы локализацию проблемного места, поэтому для сброса переменной _LastError в 0 существует отдельная функция ResetLastError.

void ResetLastError()

Функция устанавливает значение встроенной переменной _LastError в ноль.

Функцию рекомендуется вызывать перед любым действием, которое может привести к ошибке, и после которого планируется проводить анализ с помощью GetLastError.

Хорошим примером использования обеих функций является уже упомянутый макрос PRTF (файл PRTF.mqh). Напомним его код:

#include <MQL5Book/MqlError.mqh>
   
#define PRTF(AResultPrint(#A, (A))
   
template<typename T>
T ResultPrint(const string sconst T retval = NULL)
{
   const int snapshot = _LastError// зафиксируем _LastError на входе
   const string err = E2S(snapshot) + "(" + (string)snapshot + ")";
   Print(s"="retval" / ", (snapshot == 0 ? "ok" : err));
   ResetLastError(); // очистка признака ошибки для следующих вызовов
   return retval;
}

Задача макроса и обернутой в него функции ResultPrint, вывести в журнал переданное значение, текущий код ошибки и тут же очистить код ошибки. Таким образом, последовательное применение PRTF на ряде инструкций всегда гарантирует, что выведенная в журнал ошибка (или признак успеха) соответствует именно последней инструкции, с помощью которой было получено значение параметра retval.

Сохранение _LastError в промежуточной локальной переменной snapshot потребовалось по той причине, что _LastError может изменить свое значение практически в любом месте вычисления выражения, если какая-либо операция выполнится с ошибкой. В частности, здесь в макросе E2S используется функция EnumToString, которая может взвести свой код ошибки, если в качестве аргумента передано значение, отсутствующее в перечислении. Тогда в последующих частях того же выражения при формировании строки будет фигурировать уже не изначальная ошибка, а наведенная.

В любой инструкции может оказаться несколько мест, где _LastError вдруг изменится. В связи с этим желательно зафиксировать код ошибки сразу после интересующего нас действия.