English 中文 Español Deutsch 日本語
Использование утверждений (assertions) при разработке программ на MQL5

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

MetaTrader 5Примеры | 10 сентября 2015, 10:42
2 594 21
Sergey Eremin
Sergey Eremin

Введение

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

Например, если предполагается что некое значение 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. Пример работы утверждения


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

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

  • Значения входных и выходных параметров, а также результирующих значений функций и методов попадают в ожидаемые интервалы.

    Пример проверки при помощи утверждений входных и выходных значений метода
    double CMyClass::SomeMethod(const double a)
      {
    //--- проверить значение входного параметра
       assert(a>=10,"")
       assert(a<=100,"")
    
    //--- рассчитать результирующее значение
       double result=...;
    
    //--- проверить результирующее значение
       assert(result>=0,"")
      
       return result;
      } 
    В данном примере предполагается, что входной параметр a не может быть меньше 10 и не может быть больше 100. Кроме того, предполагается, что результирующее значение не может быть меньше нуля.

  • Границы массива находятся в ожидаемом диапазоне.

    Пример проверки при помощи утверждений того, что границы массива попадают в ожидаемый диапазон
    void CMyClass::SomeMethod(const string &incomingArray[])
      {
    //--- проверяем границы массива
       assert(ArraySize(incomingArray)>0,"")
       assert(ArraySize(incomingArray)<=10,"")
    
       ...
      }
    В данном примере предполагается, что массив incomingArray может содержать в себе не меньше одного элемента, но и не больше десяти.

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

    Пример проверки при помощи утверждений того, что описатель созданного объекта не является нулевым
    void OnTick()
      {
    //--- создаем объект a
       CMyClass *a=new CMyClass();
    
    //--- какие-то действия
       ...
       ...
       ...
    
    //--- проверяем, что объект a все еще существует
       assert(CheckPointer(a),"")
    
    //--- удаляем объект a
       delete a;
      } 
    В данном примере предполагается, что в конце выполнения OnTick объект a все еще существует.

  • Является ли делитель в операции деления не нулем.

    Пример проверки при помощи утверждений делителя на неравенство нулю
    void CMyClass::SomeMethod(const double a, const double b)
      {
    //--- проверяем, что b не равно нулю
       assert(b!=0,"")
    
    //--- выполняем деление a на b
       double c=a/b;
      
       ...  
       ...
       ...
      } 
    В данном примере предполагается, что входной параметр b, на который делится a, не равен нулю.

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

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

Предусловия — это соглашения, которые клиентский код, вызывающий метод или класс, обещает выполнить до вызова метода или создания экземпляра объекта. Иными словами, если предполагается, что метод должен принимать некий параметр со значениями больше 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, а также даны общие рекомендации по его применению. Грамотно применяемые утверждения позволяют значительно упростить процесс разработки и отладки программного обеспечения.

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

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

Прикрепленные файлы |
assert.mqh (1.01 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (21)
Sergey Eremin
Sergey Eremin | 17 сент. 2015 в 09:38
Rashid Umarov:

Возможно, пригодится и такой макрос

Кстати, считаю, что было бы неплохо дополнить документацию про # и про многострочные макросы. Кто Си не учил, того могут немножко обескуражить коды в статье и комментариях :)
Dina Paches
Dina Paches | 17 сент. 2015 в 10:16
Rashid Umarov:

Возможно, пригодится и такой макрос

Не возможно, а действительно такое нужнО в хозяйстве. Спасибо.

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

И, предполагаю, видимо, и Сергей имел в виду что-то наподобие такого, когда писал здесь.

Однако мне ещё надо для самой себя со многим разобраться/знакомиться применительно таких построений.

В том числе, при применении преобразования данных перед выводом на печать (и с учётом вывода на печать не только через Print, но и в комментарий или алерт). И чтобы название переменной не терялось визуально на фоне DoubleToString, к примеру, или TimeToString.

То есть, сейчас записала,  к примеру, так:

#define TEST_PRINT(x)   Print(__LINE__,", ",__FUNCTION__,", ",#x,"=",x)
#define TEST_COMMENT(x) Comment(__LINE__,", ",__FUNCTION__,", ",#x,"=",x)
#define TEST_ALERT(x)   Alert(__LINE__,", ",__FUNCTION__,", ",#x,"=",x)

Во вкладке "Эксперты" это отображается так:


А на чарте такой же коммент, в случае применения.

То есть, переменная price_0 "теряется" на фоне DoubleToString.

Однако делать в коде с таким #define вывод тестовых строк уже проще, чем я приводила здесь ранее. Хотя пока это всё равно не будет ещё оптимальным вариантом.


P./S.: Сейчас подумалось, что то, что и применение преобразующих функций выводится и наглядно видно в тестовом тексте - это, наоборот, может быть полезным. В общем, не настолько сильно наименование переменной и теряется. Просто ранее для себя это не предполагалось и не привычно для глаз при выводе инфы.


Sergey Eremin:
Кстати, считаю, что было бы неплохо дополнить документацию про # и про многострочные макросы. Кто Си не учил, того могут немножко обескуражить коды в статье и комментариях :)
Это было бы здорово.
Dina Paches
Dina Paches | 17 сент. 2015 в 11:00
P./S.: Сейчас чем больше смотрю на вывод инфы, тем больше осознаю, что, да, вывод и функций, преобразующих данные из одного формата в другой - это однозначно удобно всё-таки в тестовых записях. Дело "за малым" - оптимизировать, по возможности, и это.
TheXpert
TheXpert | 17 сент. 2015 в 11:07
Andrey Shpilev:

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

Тот случай когда макросы безальтернативный вариант
mktr8591
mktr8591 | 13 апр. 2021 в 19:38

Если кто-то собирается использовать этот код, имейте в виду такой момент: следующий скрипт

   if(true)
      assert(1==1, "")
   else
      Print("Never executed");

приводит к выводу сообщения "Never executed" из ветки else.

Чтобы можно было использовать assert корректно, нужно исправить его, например в таком виде:

#define assert(condition, message) \
       do 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; \
        } while(false)
#else
#define assert(condition, message) 
#endif

(в ветке #else также макрос скорректирован : возвращает пустую строку (а не ";").

В таком варианте после assert(..) нужно ставить ";"

Индикатор для построения графика "шпинделей" (веретён) Индикатор для построения графика "шпинделей" (веретён)
Статья рассматривает построение графика "шпинделей" (spindles) или, как их еще называют, "веретён", его использование в торговых стратегиях и советниках. Вначале обсудим появление графика, его построение и связь с графиком японских свечей. Далее проанализируем реализацию индикатора в программном коде на языке MQL5. Протестируем основанный на индикаторе эксперт и сформулируем торговую стратегию.
Введение в теорию нечеткой логики Введение в теорию нечеткой логики
Нечеткая логика расширяет привычные нам границы математической логики и теории множеств. В статье раскрыты основные принципы этой теории, а также описаны две системы нечеткого логического вывода типа Мамдани и Сугено. Приведены примеры реализации нечетких моделей на основе этих двух систем средствами библиотеки FuzzyNet для MQL5.
Работаем с ZIP-архивами средствами MQL5 без использования сторонних библиотек Работаем с ZIP-архивами средствами MQL5 без использования сторонних библиотек
Язык MQL5 развивается, и в него постоянно добавляются новые функции для работы с данными. С некоторых пор, благодаря нововведениям, стало возможно работать с ZIP-архивами штатными средствами MQL5 без привлечения сторонних библиотек DLL. Данная статья подробно описывает, как это делается, на примере описания класса CZip — универсального инструмента для чтения, создания и модификации ZIP-архивов.
Теория рынка Теория рынка
До сих пор не существует логически завершенной теории рынка, охватывающей все типы и разновидности рынков товаров и услуг, микро- и макро-рынков, наподобие Форекс. Статья повествует о сущности новой теории рынка, основанной на анализе прибыли, вскрывает закономерности изменения текущей цены, а также выявляет принцип работы механизма, позволяющего цене находить наиболее оптимальное свое значение путем образования цепи виртуальных цен, способных вырабатывать управляющие воздействия на саму цену. Выявлены механизмы образования и смены трендов на рынке.