文章 "在MQL5程序中使用断言"

 

新文章 在MQL5程序中使用断言已发布:

本文介绍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 呢?1,我完全不知道。

另外,所有的语句通常都是一样的,就像在 C++ 中一样,语句都是写在宏上的,写法和我走的路是一样的。我发现的唯一弱点是:如果在使用宏的存储过程/函数中声明了名为x 的输入参数或变量,就会收到警告。解决方法很简单:在宏中给数组取一个更独特的名字,例如assertionFailedArray


2.我看不出有什么区别。执行错误就是执行错误,它会导致程序崩溃,无法继续执行。不过,我还是要回答一下为什么要这样做:起初是除以 0,但我在测试这样一个宏时,由于某种原因,在方法中调用它时代码执行不会中断。如果是在 OnTick、OnInit 等方法中调用,那么是的,执行会停止。如果在任意类的某个方法中调用,则不会。至于是否是 MQL5 的错误,我没有费心去研究,我只是开始调用另一个执行错误 :).

我会试着看看在方法中除以 0 有什么问题。

 
我也不知道为什么(毕竟说的是调试),我就把这段代码留在这里吧:
#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:

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

   //--- 输出数据
   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)); }

   //--- 行尾输出(实际输出/调用打印函数)
   CFormatOut *operator<<(CFormatOutEol &eol) { Print(m_line); m_line=NULL; return(GetPointer(this)); }

   //--- 更改实数的输出格式
   CFormatOut *operator<<(CFormatOutFmtDigits &fmt) { m_dbl_fmt="%."+(string)fmt.digits+"f"; return(GetPointer(this)); }
   
   //--- 添加/删除自动空格嵌入符
   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;
//--- 在输出中插入结束行的特定对象
CFormatOutEol        EOL;
//--- 设置数字输出的位数
CFormatOutFmtDigits  DBL_FMT_DIGITS(int digits) { CFormatOutFmtDigits fmt; fmt.digits=digits; return(fmt); }
//--- 开/关在输出之间插入空格
CFormatOutFmtSpace   AUTO_SPACE(bool enable)    { CFormatOutFmtSpace  fmt; fmt.space =enable; return(fmt); }
//--- 将枚举转换为字符串的简易函数
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;
Result:
2015.09.01 18:04:49.060    Test EURUSD,H1: 3.1416 Test 3.141593 PERIOD_M1

注意:
表达式 OUT << ... 的参数顺序相反,从右向左,可能会产生副作用的参数从右到左顺序相反,可能会产生副作用!
 
Ilyas:
我不知道为什么(我们在讨论调试),所以我就把这段代码留在这里了:

在我看来,如果能在代码中指定输出位置(通过Print 输出到日志、警报、文件等),可能会更有用。尤其是这样做并不难。


附:我能批评/赞扬一下这篇文章吗?:)

 
Sergey Eremin:

如果您能在代码中指定输出位置(通过 Print 输出到日志、警报、文件等),在我看来可能会更有用。尤其是这样做并不难。


附:我能批评/赞扬一下这篇文章吗?:)

这篇文章只讨论了 DEBUG ASSERT,有它在手是件好事。

但是,IMHO!

一个好的日志记录器应该有一个日志记录级别:
  1. FATAL - ошибка, дальнейшее выполнение программы невозможно
  2. ERR   - ошибка, выполнение программы можно продолжить
  3. ATT   - предупреждение
  4. MSG   - сообщение

当程序正在调试 时,日志记录器会调用 DebugBreak- 您可以停止并查看 MQL 程序的环境(状态)。
当程序在终端用户处运行时,日志记录器信息会保存到文件(打印/警报)
默认情况下,日志只输出 ERR 和 FATAL 错误,用户可以在运行程序时更改日志级别,以查看程序的所有信息(ATT 和 MSG)。
如果使用得当,日志可用于识别/查找程序中的错误。
 
这是你的骨头,给它们加点肉吧:
#property script_show_inputs

enum EnLogLevel
  {
   __LOG_LEVEL_FATAL,   // 只有致命错误
   __LOG_LEVEL_ERR,     // 只有错误
   __LOG_LEVEL_ATT,     // 警告和错误
   __LOG_LEVEL_MSG,     // 所有信息
  };

input EnLogLevel LogLevel=__LOG_LEVEL_MSG;   // 记录仪级别
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
#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;
//+------------------------------------------------------------------+
//| 脚本程序启动功能|
//+------------------------------------------------------------------+
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,有它在手是件好事。 但是,IMHO! 一个好的日志记录器必须有日志记录级别。日志记录级别可由 MQL 程序参数控制:




  1. FATAL - ошибка, дальнейшее выполнение программы невозможно
  2. ERR   - ошибка, выполнение программы можно продолжить
  3. ATT   - предупреждение
  4. MSG   - сообщение

当程序正在调试 时,日志记录器会调用 DebugBreak- 您可以停止并查看 MQL 程序的环境(状态)。
当程序在最终用户处运行时,日志记录器信息会保存到文件(打印/警报)
默认情况下,日志只生成 ERR 和 FATAL 错误,用户可以在运行程序时更改日志级别,以查看程序的所有信息(ATT 和 MSG)。
如果使用得当,日志可用于识别/查找程序中的错误。

就在下一篇文章中(如果 Rashid 同意),我计划对已发布版本软件中的 "预期 "错误进行处理(作为批准后的逻辑延续),其中将包括日志问题的披露。

非常感谢你的这两条评论,如果你不介意的话,我将把它们用于这篇文章。

 
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 很容易,但删除 Prints 本身却不容易。