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

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

MetaTrader 5Примеры |
2 726 28
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)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (28)
Alain Verleyen
Alain Verleyen | 6 дек. 2015 в 11:34
Sergey Eremin:

Пожалуйста, покажите мне пример, как выйти с помощью ExpertRemove() в цикле.

Например, у нас есть такой код:

Нам нужно выйти, если i == 2, а все остальные шаги не должны выполняться. В журнале мы должны видеть только "0" и "1". Как это можно сделать с помощью данной функции?

Сейчас ExpertRemove() не останавливает советника в нужный момент, все шаги будут выполняться, а после этого советник будет остановлен. Но это неправильно для механизма Assertion, мы должны останавливать советника мгновенно. И да, мы не можем использовать только "break", потому что нам нужен универсальный макрос или функция для любой части любого советника.

Вам не нужно использовать ExpertRemove() в OnInit(), просто используйте return(INIT_FAILED);

int OnInit()
  {
//---
    ...

         if(somethign wrong)
           {
            //ExpertRemove(); 
            return(INIT_FAILED);    //--- Нет необходимости использовать ExpertRemove() в OnInit()
           }
    ...
  }

в других частях кода просто возвращайте :

            ExpertRemove();
            return;           //--- Просто вернитесь, чтобы завершить обработчик текущего события

or

            ExpertRemove();
            return(x);        //--- Просто вернитесь, чтобы завершить обработчик текущего события

Насчет индикаторов - пожалуйста, покажите мне универсальный механизм для определения ShortName индикаторов. Потому что без этого механизма мы не сможем использовать эту функцию для Assertions. Да, мы можем определить ShortName в нашем конкретном индикаторе (например, с помощью глобальной переменной, как это делают многие, хотя это плохая практика), но у нас нет, например, универсальной функции "GetShortName()". Поэтому мы не можем сделать универсальный механизм (я имею в виду какой-то макрос или функцию для абсолютно любых индикаторов, где мы можем добавить всего одну строчку "assert(...)") с ChartIndicatorDelete().

В чем проблема? Вы работаете над своим индикатором, это ваш код, поэтому вы знаете его короткое название.

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

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

И покажите мне, пожалуйста, "тривиальный" вариант для скриптов для любых частей кода. Это должна быть одна (!) функция или макрос для любой части скрипта:

1) Для циклов
2) Для функций с любым возвращаемым типом
3) Для функций без возвращаемого типа (void).

Таким образом, мы должны просто добавить одну строку "assert(...)" в любую часть Sript, вот так:

Аналогично для EA.

Sergey Eremin
Sergey Eremin | 6 дек. 2015 в 12:30
Alain Verleyen:

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

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

Хорошо, я понял вас. Спасибо, вы правы.

Но в своей статье я имею в виду решения для Assertions: универсальный механизм остановки MQL4/5 приложений в любом месте кода (включая OnInit и cicles). Просто добавьте одну строчку в любой части и готово. Как это работает в любом механизме Assertions во многих языках программирования ;)

Да, ваши варианты верны. Но не для моего видения Assertions, потому что это не универсальное решение для любой части любого кода.

Спасибо за ваш пример с EA.

Alain Verleyen
Alain Verleyen | 6 дек. 2015 в 13:20
Sergey Eremin:

Хорошо, я понял вас. Спасибо, вы правы.

Но в своей статье я имел в виду решения для Assertions: универсальный механизм остановки MQL4/5 приложений в любом месте кода (включая OnInit и циклы). Просто добавьте одну строчку в любой части и готово. Как это работает в любом механизме Assertions во многих языках программирования ;)

Да, ваши варианты верны. Но не для моего видения Assertions, потому что это не универсальное решение для любой части любого кода.

Спасибо за ваш пример с EA.

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

Проанализировав контекст вызова в вашем макросе (макросах), определив, является ли он советником или индикатором, и разобрав __FUNCSIG__.

Как сделать это универсальным механизмом - решать вам.

Sergey Eremin
Sergey Eremin | 6 дек. 2015 в 13:36
Alain Verleyen:

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

Анализируя контекст вызова в вашем макросе (макросах), определяя, является ли это советником или индикатором, и разбирая __FUNCSIG__.

Как сделать это универсальным механизмом - решать вам.

Да, сначала я думал о таких вещах, но в итоге сделал так, как мы видим в статье :)

Спасибо за комментарии!

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-архивов.
Теория рынка Теория рынка
До сих пор не существует логически завершенной теории рынка, охватывающей все типы и разновидности рынков товаров и услуг, микро- и макро-рынков, наподобие Форекс. Статья повествует о сущности новой теории рынка, основанной на анализе прибыли, вскрывает закономерности изменения текущей цены, а также выявляет принцип работы механизма, позволяющего цене находить наиболее оптимальное свое значение путем образования цепи виртуальных цен, способных вырабатывать управляющие воздействия на саму цену. Выявлены механизмы образования и смены трендов на рынке.