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

 

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

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

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

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

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

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

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

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

Автор: Sergey Eremin

 

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

2. Какой-то слишком "грязный трюк" с массивом. Нельзя было на ноль поделить?

 
Andrey Shpilev:

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

2. Какой-то слишком "грязный трюк" с массивом. Нельзя было на ноль поделить?

1. Чтобы не быть голословным, покажите пример условия, которое моему макросу нельзя скормить (это я без сарказма, мне действительно очень важно знать обо всех тонких местах, т.к. я постоянно использую этот макрос). Ну и поясните, в чём сложности дебага?

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

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

Буду очень признателен (и наверняка не я один), если Вы покажете свой вариант в виде процедуры, который всё это будет реализовывать (разумеется, "из коробки" и на автомате, а не путём ручной передачи всего этого как параметров). В принципе 2..4 можно передавать как входные параметры, и это будет более-менее универсально (в том сысле что всегда будет одно и то же передаваться, не нужно будет ручками что-то задавать), а вот как п. 1 получить в процедуре у меня вообще нет идей

Плюс всё таки обычно, как в том же C++, утверждения написаны на макросах, по этому же пути и я пошёл. Единственное слабое место, которое я вижу: если в процедуре/функции, в которой используем такой макрос, объявлен входной параметр  или переменная с именем x, то получим предупреждение. Решается просто: в макросе назвать массив как-нибудь более уникально, к примеру assertionFailedArray.


2. Не вижу разницы. Ошибка исполнения на то и ошибка исполнения, что аварийно грохнет программу и она дальше не будет выполняться. Однако, отвечу почему именно по такому пути пошёл: сначала было деление на ноль, но когда тестировал такой макрос почему-то у меня при вызовах его в методах не прерывалось исполнение кода. Если в OnTick, OnInit и т.п., то да, исполнение останавливалось. Если внутри некоторого метода произвольного класса, то нет. Ошибка ли MQL5 это я не стал разбираться, просто стал вызывать другую ошибку исполнения :)

Постараюсь по возможности посмотреть поподробней что не так с делением на ноль в методах.

 
Не знаю зачем (разговор про отладку же), просто оставлю здесь этот код:
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct CFormatOutEol       { uchar dummy; };
struct CFormatOutFmtDigits { int digits;  };  
struct CFormatOutFmtSpace  { bool space;  };  
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CFormatOut
  {
   string            m_line;
   string            m_dbl_fmt;
   bool              m_auto_space;

public:

   //---- constructor
                     CFormatOut(int dbl_fmt_digits=4,bool auto_space=false):m_dbl_fmt("%."+(string)dbl_fmt_digits+"f") { }

   //--- output data
   CFormatOut *operator<<(double x) { auto_space(); m_line+=StringFormat(m_dbl_fmt,x); return(GetPointer(this)); }
   CFormatOut *operator<<(string s) { auto_space(); m_line+=s;                         return(GetPointer(this)); }
   CFormatOut *operator<<(long   l) { auto_space(); m_line+=(string)l;                 return(GetPointer(this)); }

   //--- output end of line (real output/call Print function)
   CFormatOut *operator<<(CFormatOutEol &eol) { Print(m_line); m_line=NULL; return(GetPointer(this)); }

   //--- change output format for real numbers
   CFormatOut *operator<<(CFormatOutFmtDigits &fmt) { m_dbl_fmt="%."+(string)fmt.digits+"f"; return(GetPointer(this)); }
   
   //--- add/remove auto space insetring
   CFormatOut *operator<<(CFormatOutFmtSpace  &fmt) { m_auto_space=fmt.space; return(GetPointer(this)); }

protected:
   void              auto_space() { if(m_line!=NULL && m_auto_space) m_line+=" "; }
  };

CFormatOut           OUT;
//--- specal object for inserting EndOfLine to output
CFormatOutEol        EOL;
//--- setting digits for numbers output
CFormatOutFmtDigits  DBL_FMT_DIGITS(int digits) { CFormatOutFmtDigits fmt; fmt.digits=digits; return(fmt); }
//--- on/off inserting spaces between outputs
CFormatOutFmtSpace   AUTO_SPACE(bool enable)    { CFormatOutFmtSpace  fmt; fmt.space =enable; return(fmt); }
//--- shorty function to convert enums to string
template<typename T> string EN(T enum_value)    { return(EnumToString(enum_value)); }
Использование:
OUT << AUTO_SPACE(true) << M_PI << "Test" << DBL_FMT_DIGITS(6) << M_PI << EN(PERIOD_M1) << EOL;
Результат:
2015.09.01 18:04:49.060    Test EURUSD,H1: 3.1416 Test 3.141593 PERIOD_M1

ВНИМАНИЕ!
Вычисление параметров выражения OUT << ... в обратном порядке, справа налево, возможен сайд эффект!
 
Ilyas:
Не знаю зачем (разговор про отладку же), просто оставлю здесь этот код:

Если в Вашем коде иметь возможность задавать куда выводить (в журнал через Print, в алерт, в файл и т.д.), то ещё полезней может получиться, мне кажется. Тем более, что сделать это совсем несложно.


P.S. а статью покритиковать/похвалить? :)

 
Sergey Eremin:

Если в Вашем коде иметь возможность задавать куда выводить (в журнал через Print, в алерт, в файл и т.д.), то ещё полезней может получиться, мне кажется. Тем более, что сделать это совсем несложно.


P.S. а статью покритиковать/похвалить? :)

В статье рассматривается только DEBUG ASSERT, иметь его на вооружении хорошо. Тема раскрыта.

Но, ИМХО! Для пользователей (масштабного использования материала статьи) необходим не только DEBUG ASSERT, но и логер.

У хорошего логера должен быть уровень логирования:
  1. FATAL - ошибка, дальнейшее выполнение программы невозможно
  2. ERR   - ошибка, выполнение программы можно продолжить
  3. ATT   - предупреждение
  4. MSG   - сообщение
Уровнем логирования можно управлять параметром MQL программы.
Когда программа отлаживается, в логере вызывается DebugBreak - можно остановиться и посмотреть на окружение(состояние) MQL программы.
Когда программа работает у конечного пользователя сообщения логера сохраняются в файл(принт/алерт).
По умолчанию, логер выдаёт только ERR и FATAL ошибки и пользователь при запуске программы всегда может изменить уровень логирования, для того, что бы были видны все сообщения программы(ATT и MSG).
При правильном использовании, по логу можно определить/найти ошибку в программе.
 
Вот Вам "кости", нарастите на них мясо:
#property script_show_inputs

enum EnLogLevel
  {
   __LOG_LEVEL_FATAL,   // fatal errors only
   __LOG_LEVEL_ERR,     // errors only
   __LOG_LEVEL_ATT,     // warnings and errors
   __LOG_LEVEL_MSG,     // all messages
  };

input EnLogLevel LogLevel=__LOG_LEVEL_MSG;   // logger level
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#define __LOG_OUT(params) ExtTrueLogger.Out params
#define __LOG(level,params) do{ if(level<=LogLevel) __LOG_OUT(params); }while(0)
#define LOG_MSG(msg)    __LOG(__LOG_LEVEL_MSG,(__FUNCSIG__,__FILE__,__LINE__,msg))
#define LOG_ATT(msg)    __LOG(__LOG_LEVEL_ATT,(__FUNCSIG__,__FILE__,__LINE__,msg))
#define LOG_ERR(msg)    __LOG(__LOG_LEVEL_ERR,(__FUNCSIG__,__FILE__,__LINE__,msg))
#define LOG_FATAL(msg)  __LOG(__LOG_LEVEL_FATAL,(__FUNCSIG__,__FILE__,__LINE__,msg))
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CTrueLogger
  {
public:
   void              Out(string func,string file,int line,string msg)
     {
      Print(func," ",func," ",file," ",line," ",msg);
     }
  } ExtTrueLogger;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   LOG_MSG("Hello MSG world!");
   LOG_ATT("Hello ATT world!");
   LOG_ERR("Hello ERR world!");
   LOG_FATAL("Hello FATAL world!");
  }
 
Ilyas:
В статье рассматривается только DEBUG ASSERT, иметь его на вооружении хорошо. Тема раскрыта.

Но, ИМХО! Для пользователей (масштабного использования материала статьи) необходим не только DEBUG ASSERT, но и логер.

У хорошего логера должен быть уровень логирования:
  1. FATAL - ошибка, дальнейшее выполнение программы невозможно
  2. ERR   - ошибка, выполнение программы можно продолжить
  3. ATT   - предупреждение
  4. MSG   - сообщение
Уровнем логирования можно управлять параметром MQL программы.
Когда программа отлаживается, в логере вызывается DebugBreak - можно остановиться и посмотреть на окружение(состояние) MQL программы.
Когда программа работает у конечного пользователя сообщения логера сохраняются в файл(принт/алерт).
По умолчанию, логер выдаёт только ERR и FATAL ошибки и пользователь при запуске программы всегда может изменить уровень логирования, для того, что бы были видны все сообщения программы(ATT и MSG).
При правильном использовании, по логу можно определить/найти ошибку в программе.

Как раз следующей статьёй (если Рашид одобрит) я задумал обработку "ожидаемых" ошибок уже в релизных версиях ПО (как логичное продолжение после утверждений), куда войдёт и раскрытие вопроса логирования.

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

 
Sergey Eremin:

Как раз следующей статьёй (если Рашид одобрит) я задумал обработку "ожидаемых" ошибок уже в релизных версиях ПО (как логичное продолжение после утверждений), куда войдёт и раскрытие вопроса логирования.

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

Конечно, буду ждать статьи, подпишусь на драфт и буду помогать в её разработке, удачи.
 
Sergey Eremin:

Интересная тематика.

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

Спс.

Заодно, скачав файл assert.mqh, добавила туда себе до кучи строчку:

#define TEST_TEXT "Line: ",__LINE__,", ",__FUNCTION__,", "

А в коде потом это, соответственно, примерно так выглядит:

  Print(TEST_TEXT,"a = ",a);

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

 
Dina Paches:

Интересная тематика.

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

Спс.

Заодно, скачав файл assert.mqh, добавила туда себе до кучи строчку:

А в коде потом это, соответственно, примерно так выглядит:

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

Спасибо за отзыв!

Для TEST_TEXT чтобы действительно было легко убирать условной компиляцией, я бы на Вашем месте рассмотрел вынос и Print'а внутрь макроса. В текущем варианте, мне кажется, легко убрать TEST_TEXT, но не сами Print'ы.