Использование утверждений (assertions) при разработке программ на MQL5

Sergey Eremin | 10 сентября, 2015

Введение

Утверждение — это специальная конструкция, которая дает возможность проверять произвольные допущения в произвольных местах программы. Обычно утверждения представляют собой некий код (чаще всего это отдельная функция или макрос). Этот код проверяет на истинность значение некоторого выражения. Если оно оказывается ложным, то выводится соответствующее сообщение и, если реализация это предусматривает, прерывается работа программы. Соответственно, если выражение истинно, значит, все работает так, как и задумывалось: допущение соблюдается. В противном случае можно быть уверенным, что в программе обнаружена ошибка, и она об этом явно сообщает.

Например, если предполагается что некое значение X в программе ни при каких обстоятельствах не должно быть меньше нуля, то можно внести такое утверждение, как «утверждаю, что значение X больше или равно нулю». В случае если X все же окажется меньше нуля, то будет выведено соответствующее сообщение и программист сможет скорректировать программу.

Утверждения особенно полезны в больших проектах, отдельные части которых могут быть либо повторно использованы, либо быть подвержены модификациям со временем.

Утверждениями следует охватывать те ситуации, которые не должны возникать при нормальной работе программы. Как правило, утверждения применяются только на этапах разработки и отладки программ, т.е. в конечной версии их не должно быть. Для этого при компиляции конечных версий убираются все утверждения. Обычно этого достигают применением условной компиляции.


Примеры реализации механизма утверждений на MQL5

Как правило, механизм утверждений предусматривает следующие возможности:

  1. Вывод текста выражения, переданного на проверку.
  2. Вывод имени файла с исходным кодом, в котором обнаружена ошибка.
  3. Вывод имени или сигнатуры функции или метода, где обнаружена ошибка.
  4. Вывод номера строки в исходном файле, в которой проверяется выражение.
  5. Вывод произвольного сообщения, которое задает программист на этапе написания кода.
  6. Остановка выполнения программы при обнаружении ошибки.
  7. Возможность исключить из компилируемой программы все утверждения при помощи условной компиляции или аналогичного механизма.

Реализовать практически все эти возможности позволяют стандартные функции (кроме п. 6 — см. далее), а также механизм макросов и условной компиляции языка MQL5. Так, два из возможных вариантов могли бы выглядеть следующим образом:

Вариант №1 (мягкий вариант, без принудительной остановки программы)

#define DEBUG

#ifdef DEBUG  
   #define assert(condition, message) \
      if(!(condition)) \
        { \
         string fullMessage= \
                            #condition+", " \
                            +__FILE__+", " \
                            +__FUNCSIG__+", " \
                            +"line: "+(string)__LINE__ \
                            +(message=="" ? "" : ", "+message); \
         \
         Alert("Assertion failed! "+fullMessage); \
        }
#else
   #define assert(condition, message) ;
#endif 

Вариант №2 (жесткий вариант, с принудительной остановкой программы)

#define DEBUG

#ifdef DEBUG  
   #define assert(condition, message) \
      if(!(condition)) \
        { \
         string fullMessage= \
                            #condition+", " \
                            +__FILE__+", " \
                            +__FUNCSIG__+", " \
                            +"line: "+(string)__LINE__ \
                            +(message=="" ? "" : ", "+message); \
         \
         Alert("Assertion failed! "+fullMessage); \
         double x[]; \
         ArrayResize(x, 0); \
         x[1] = 0.0; \
        }
#else 
   #define assert(condition, message) ;
#endif


Устройство макросов assert

Сначала объявляется идентификатор DEBUG. Если этот идентификатор оставить объявленным, то будет иметь силу ветка #ifdef выражения условной компиляции, и в программу будет включен полнофункциональный макрос assert. В противном случае (ветка #else) в программу будет включен макрос assert, который не будет приводить к выполнению каких-либо действий.

Полнофункциональный макрос assert построен следующим образом. Сначала выполняется проверка входящего выражения condition. Если оно является ложью, то формируется сообщение fullMessage и выполняется его вывод. Сообщение fullMessage строится из следующих элементов:

  1. Текст выражения, передаваемого на проверку (#condition).
  2. Имя файла с исходным кодом, из которого был вызван макрос (__FILE__).
  3. Сигнатура функции или метода, из которого был вызван макрос (__FUNCSIG__).
  4. Номер строки в файле с исходным кодом, на которой расположен вызов макроса (__LINE__).
  5. Сообщение, переданное в макрос, если оно не пустое (message).

После вывода сообщения (Alert) во втором варианте макроса выполняется попытка присвоить значение несуществующему элементу массива, что приводит к ошибке времени выполнения и немедленному аварийному завершению программы.

Данный подход к завершению работы программы имеет побочный эффект для индикаторов, которые работают в своих подокнах: их подокна остаются в терминале, соответственно, закрывать их приходится вручную. Кроме того, возможны артефакты в виде неудаленных графических объектов, глобальных переменных терминала, файлов и т.п., которые создавались в процессе работы программы до момента аварийного завершения. Если подобное поведение категорически неприемлемо, то следует использовать первый вариант макроса.

Пояснение. На момент написания статьи в MQL5 не было предусмотрено механизма немедленной аварийной остановки программы. В связи с этим применен такой «грязный» трюк, как вызов ошибки времени выполнения, который гарантировано аварийно завершит программу.

Такой макрос можно поместить в отдельный включаемый файл assert.mqh, который будет расположен, к примеру, в <каталог данных терминала>/MQL5/Include. Данный файл (вариант №2) прикреплен к статье.

Ниже показан код с примером использования утверждения и результат работы данного кода.

Пример использования макроса assert в коде эксперта

#include <assert.mqh>

int OnInit()
  {
   assert(0 > 1, "my message")   

   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {  
  }

void OnTick()
  {
  }

Здесь описано утверждение, которое буквально означает «утверждаю, что 0 больше 1». Естественно, утверждение ложно, что и приводит к выводу сообщения об ошибке:

Рисунок 1. Пример работы утверждения

Рис. 1. Пример работы утверждения


Общие принципы применения утверждений

Утверждения следует применять для выявления непредвиденных ситуаций в работе программы, а также для документирования и контроля над исполнением принятых допущений. К примеру, утверждения можно применять для проверки следующих условий:

Безусловно, типов условий, которые не будет лишним проверять при помощи утверждений, намного больше и в каждом случае они будут индивидуальны. Выше показаны лишь некоторые из таковых.

Используйте утверждения для проверки предусловий и постусловий. Существует такой подход к проектированию и разработке программного обеспечения, как «Проектирование по контракту». Согласно данному подходу, каждая функция, метод или класс заключают контракт с остальной частью программы посредством пред- и постусловий.

Предусловия — это соглашения, которые клиентский код, вызывающий метод или класс, обещает выполнить до вызова метода или создания экземпляра объекта. Иными словами, если предполагается, что метод должен принимать некий параметр со значениями больше 10, то программист должен озаботиться о том, чтобы вызывающий его код ни при каких обстоятельствах не передавал в этот параметр значение меньше или равно 10.

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

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

По возможности применяйте утверждения для проверки ошибок программирования, т.е. ошибок, на которые не влияют внешние факторы. В свою очередь, для ошибок, которые могут быть вызваны внешними и не зависящими от программиста факторами, их применять не стоит. Так, не лучшей идеей будет использование утверждений для проверки корректности открытия сделки, доступности истории котировок за некий требуемый период и т.п. Для таких ошибок больше подойдет их обработка и логирование.

Не допускайте помещения исполняемого кода в утверждения. Поскольку все утверждения могут быть удалены на этапе компиляции окончательной версии программы, то ни в коем случае они не должны влиять на поведение программы. К примеру, часто подобная проблема связана с тем, что внутри assert производится вызов какой-то функции или метода.

Утверждение, которое может повлиять на поведение программы после отключения всех утверждений

void OnTick()

  {
   CMyClass someObject;

//--- проверить на корректность некоторые расчеты
   assert(someObject.IsSomeCalculationsAreCorrect(),"")
  
   ...
   ...
   ...
  }

В данном случае следует вынести вызов функции до утверждения, сохранить ее результат в некую статусную переменную и уже ее проверять в утверждении:

Утверждение, которое не может повлиять на поведение программы после отключения всех утверждений

void OnTick()
  {
   CMyClass someObject;

//--- проверить на корректность некоторые расчеты
   bool isSomeCalculationsAreCorrect = someObject.IsSomeCalculationsAreCorrect();
   assert(isSomeCalculationsAreCorrect,"")
  
   ...
   ...
   ...
  }

Не путайте утверждения и обработку ожидаемых ошибок. Утверждения служат для поиска ошибок в программах на этапе разработки и отладки (поиск ошибок программирования), а обработка ожидаемых ошибок служит бесперебойной работе уже выпущенной версии программы (в идеале это ошибки, которые не являются ошибками программирования). Утверждения никак не должны обрабатывать ошибку, они должны просто кричать «эй, дружище, у тебя вот в этом месте ошибка!».

Например, в случае если некий метод класса требует, чтобы ему передали значение больше 10, а программист пытается передать ему 8, то это явная ошибка программиста, которую было бы правильно предупредить утверждением:

Пример вызова метода с недопустимым значением входного параметра (для проверки значения параметра применяется утверждение)

void CMyClass::SomeMethod(const double a)

  {
//--- проверяем, что a больше 10
   assert(a>10,"")
  
   ...
   ...
   ...
  }

void OnTick()
  {
   CMyClass someObject;

   someObject.SomeMethod(8);
  
   ...
   ...
   ...
  }

Теперь, когда программист запустит код с передачей значения 8, программа ему явно сообщит «утверждаю, что в этот метод нельзя передавать значения меньше или равные 10!».

Рисунок 2. Результат работы вызова метода с недопустимым значением входного параметра (для проверки значения параметра применяется утверждение)

Рис. 2. Результат работы вызова метода с недопустимым значением входного параметра (для проверки значения параметра применяется утверждение)

После подобного сообщения программист сможет достаточно оперативно исправить ошибку.

В свою очередь, если для работы программы необходимо, чтобы история по инструменту была более чем за 1000 баров, то обратная ситуация ни в коем случае не может быть рассмотрена как ошибка программиста, ведь объем доступной истории не может зависеть от него. Ситуацию, когда по инструменту доступно менее 1000 баров логично было бы обработать как ожидаемую ошибку:

Пример обработки ситуации, когда доступной истории по инструменту меньше, чем необходимо (для такой ситуации применяется обработка ошибки)

void OnTick()
  {
   if(Bars(Symbol(),Period())<1000)
     {
      Comment("Недостаточно истории для корректной работы программы");
      return;
     }
  }

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

Пример совместного использования и утверждений, и обработки ошибок

double CMyClass::SomeMethod(const double a)
  {
//--- проверить значение входного параметра утверждением
   assert(a>=10,"")
   assert(a<=100,"")
  
//--- проверить значение входного параметра и скорректировать его, если необходимо
   double aValue = a;

   if(aValue<10)
     {
      aValue = 10;
     }
   else if(aValue>100)
     {
      aValue = 100;
     }

//--- рассчитать результирующее значение
   double result=...;

//--- проверить результирующее значение утверждением
   assert(result>=0,"")

//--- проверить результирующее значение и скорректировать его, если необходимо
   if(result<0)
     {
      result = 0;
     }

   return result;
  } 


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

Возможно множество способов обработки ошибок: от корректировки неверных значений (как в примере выше) до полной остановки выполнения. Однако их рассмотрение выходит за рамки данной статьи.

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


Заключение

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

Следует помнить о том, что утверждения направлены в первую очередь на поиск ошибок программирования (ошибок, допущенных программистом), а не ошибок, не зависящих от программиста. Утверждения не включаются в конечную версию программы. Для возможных ошибок, не зависящих от программиста, лучше применять обработку ошибок.

Механизм утверждений тесно связан с таким вопросом, как тестирование программного обеспечения. Впрочем, и обработка ошибок, и тестирование программного обеспечения — объемные темы, которые заслуживают рассмотрения в отдельных статьях.