English Русский Español Deutsch 日本語
在MQL5程序中使用断言

在MQL5程序中使用断言

MetaTrader 5示例 |
2 898 28
Sergey Eremin
Sergey Eremin

简介

断言是一种特殊的结构,使程序能够在任何地方对任意的假设进行检查。它们通常以代码的形式被包含在程序中(大多数情况下作为独立的函数或者宏)。代码检查特定的表达式是否为真。如果为假,则显示一条相应的消息,如果有需要可使程序停止运行。如果表达式为真,这说明所有的操作都在计划中 — 假设被实现。否则,你可以肯定程序出现错误,并且有关于此次报错的清晰提示。

例如,如果预期特定的值X在任何情况下都不应该小于零,则可以做出下面的声明:“我确定X的值超过或者等于零”。如果X小于零,那么一个相关信息将会显示,程序员就可以根据其来调整程序。

断言在大型项目中尤其有用,其组成部件可以重复使用或修改。

断言应仅包括在正常运行过程中程序不应该出现的情况。作为一个共识,断言只能存在于程序的开发和调试阶段,例如,它们不能出现在程序的最终版本中。所有断言必须在程序的最终版编译时删除。这一般通过条件编译来实现。


MQL5中断言机制的例子

下面的能力一般由断言机制提供:

  1. 显示被检测到的表达式文本。
  2. 当一个错误被检测到时显示源代码的文件名称。
  3. 当一个错误被检测到时显示函数或功能的名称和签名。
  4. 当表达式被检查时显示其在源文件中的行号。
  5. 显示由程序员在代码编写阶段指定的任意消息。
  6. 当发现错误时程序终止。
  7. 使用条件编译或者类似的机制能够从已编译的程序中清除所有断言。

几乎所有的功能都能使用MQL5语言中标准函数(除了第六点 — 今后再实现)条件编译机制来实现。例如,所有可选方案中的两种如下:

可选方案N1(温和版,不终止程序)

#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 

可选方案N2(强硬版本,终止程序)

#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表达式被执行。如果为false,那么fullMessage消息形成并展示出来。fullMessage由下面的元素构成:

  1. 检查表达式文本(#condition)。
  2. 调用宏的源代码的文件名(__FILE__)。
  3. 调用宏的函数或方法名称(__FUNCSIG__)。
  4. 调用宏的源代码文件的行号(__LINE__)。
  5. 传入宏的信息,如果不为空(message)。

在显示完一条信息后(Alert)在第二个宏类型中对一个不存在的数组元素进行赋值,这将导致执行报错和程序的立即崩溃。

用这个方法停止程序对于在子窗口运行的指标有副作用:程序终止时它们仍旧留在界面上,因此得手动关闭。另外,可能存在在程序的执行阶段产生的,无法移除的图形对象,如终端的全局变量,文件等。如果这个完全不能被接受,那么应该使用第一个宏。

解释。在撰写本文时MQL5还没有能够紧急停止程序的机制。作为一种替代方式,运行错误被触发,确保程序崩溃。

这个宏能够独立的存放在包含文件assert.mqh中,例如放在<data folder>/MQL5/Include文件夹下。此文件(可选方式N2)作为本文附件。

下面的代码含有一个使用断言的例子以及其执行结果。

在EA中使用assert宏的例子

#include <assert.mqh>

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

   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {  
  }

void OnTick()
  {
  }

这里你可以发现一个断言,字面意思为“我相信0比1大”。这个断言显然为false,导致显示了一个错误信息:

图 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数组至少有一个元素,但是不能超过10个。

  • 创建的对象描述符是否有效

    使用断言来检查所创建对象的描述符是否有效的例子
    void OnTick()
      {
    //--- 创建对象a
       CMyClass *a=new CMyClass();
    
    //--- 一些操作
       ...
       ...
       ...
    
    //--- 检查对象是否仍旧存在
       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做除法操作,b不等于零。

当然那还有很多类型的条件值得用断言来效验,每一种情况都是完全不同的。其中的一些已经在上面介绍了。

使用断言检查前置条件和后置条件。有一种程序设计和开发方法叫做:“契约式设计”。根据这种方法,每一个函数、方法和类使用前值和后置条件同程序剩余部分订立一个契约。

前置条件是在调用方法或创建对象实例前,同意执行方法和类调用的客户端代码协议。换句话说,如果假设一个方法的特定参数值应大于10,那么编程者必须注意调用的代码时确保在任何情况下都不应该传入一个小于或者等于10的参数。

后置条件是在完成前同意执行一个方法或者类的协议。因此,如果预期一个方法不应该返回小于100的值,那么编程者必须注意返回值使其不要小于或等于100。

记录先决条件和后置条件,同时在程序开发和调试阶段监控它们的合规情况是非常方便。有别于传统的备注,断言不仅仅声明异常,它能不断监控程序的执行情况。使用断言检查和记录前值条件和后置条件的一个例子如上图所示,请阅读“使用断言来检查输入和输出方法值的例子”。

如果有机会,使用断言来检查不受外部因素影响的程序错误。因此,最好不要用断言来检查开仓的正确性和请求特定周期的报价历史数据是否存在等。处理和记录这类报错更为合适。

避免在断言中放置可执行代码。因为所有的断言都能在程序最终版本的编译时删除,因此它们不应影响程序的执行。例如,在assert中调用函数或方法经常会遇到这个问题。

在禁用所有断言后会影响程序执行的断言

void OnTick()

  {
   CMyClass someObject;

//--- 检查某些运算的正确性
   assert(someObject.IsSomeCalculationsAreCorrect(),"")
  
   ...
   ...
   ...
  }

在这种情况下你应该将函数调用放在断言之前,将函数返回值保存在一个特定的状态变量中,然后在断言中检查之:

不能在禁用所有断言后影响程序的执行

void OnTick()
  {
   CMyClass someObject;

//--- 检查某些运算的正确性
   bool isSomeCalculationsAreCorrect = someObject.IsSomeCalculationsAreCorrect();
   assert(isSomeCalculationsAreCorrect,"")
  
   ...
   ...
   ...
  }

不要将断言和处理预期错误搞混。断言用于在程序开发和测试阶段查找错误时使用(查找程序错误)。处理预期错误,另一方面,使程序的发布版本运行平稳(理想情况不应该是程序错误)。断言不应该用于处理错误,它们应大叫:“hey,朋友,你这里有一个错误!”。

例如,需要传入一个超过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个bar,但是如果数据不足的情况出现,那么这不是程序员的错误,因为可用的历史数据不取决于他。将一个交易标的的历史数据少于1000个bar的情况,作为预期错误来处理,是符合逻辑的:

对历史数据少于所需的情况进行处理的例子(在这种情况下应用报错处理)

void OnTick()
  {
   if(Bars(Symbol(),Period())<1000)
     {
      Comment("Insufficient history for correct operation of the program");
      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;
  } 


因此,断言有助于在程序最终版本发布前追踪某些错误。即使存在那些在开发和调试阶段无法找出的bug,在最终版本中进行报错处后理程序也能正常运行。

处理报错有很多种方法:从修正错误的值(如上面的例子所示)到完全停止程序的运行。对它们进行全面的研究超出了本文的范围。

人们也确信,如果希望程序能以适当方式终止和指出错误所在来作为对报错的回应,那么断言在这种情况就显得多余了。例如,如果程序的除数为零,MQL5程序将终止执行并且在日志中显示一条相关的信息。原则上,要找到问题所在,这种方式比断言更合适。然而,断言允许在代码中添加关于假设的重要信息,这将比经典的在源码中进行备注更加明显(产生冲突的情况)和恰当,并且有助于后续的维护和代码开发。


总结

本文研究了断言机制,提供了一个其在MQL5中实现的例子,并且给出了其使用场景的一般建议。恰当的应用断言可以极大的简化软件开发和调试过程。

请注意断言首先是定位于查找程序错误(由开发者带入的错误),而不是同开发者无关的错误。断言不应在程序的最终版本中存在。对于可能存在的不取决于程序员自身的错误,最好使用错误处理机制。

断言机制同程序的测试紧密相关。报错处理及程序测试是非常大的课题,它们因被给予额外的关注并且单独成文来阐述。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/1977

附加的文件 |
assert.mqh (2.03 KB)

该作者的其他文章

最近评论 | 前往讨论 (28)
Alain Verleyen
Alain Verleyen | 6 12月 2015 在 11:34
Sergey Eremin:

请举例说明,如何在 cicle 中使用 ExpertRemove() 退出。

例如,我们有这样一段代码:

我们需要在 i == 2 的情况下退出,而所有其他步骤都必须停止运行。在日志中,我们必须只看到 "0 "和 "1"。如何使用该函数实现这一目标?

现在,ExpertRemove() 不会在需要的时候停止 EA,所有步骤都将运行,之后 EA 将停止。但这对于断言机制来说是错误的,我们必须立即停止 EA。是的,我们不能只使用 "break",因为我们需要通用宏或函数来处理任何 EA 的任何部分。

您不必在 OnInit() 中使用ExpertRemove(),只需使用 return(INIT_FAILED);

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

         if(somethign wrong)
           {
            //ExpertRemove(); 
            return(INIT_FAILED);    //--- 无需在 OnInit() 中使用 ExpertRemove()
           }
    ...
  }

在代码的其他部分,只需返回 :

            ExpertRemove();
            return;           //--- 只需返回即可完成当前事件处理程序

or

            ExpertRemove();
            return(x);        //--- 只需返回即可完成当前事件处理程序

关于指标 - 请向我展示定义指标 ShortName 的通用机制。因为如果没有这种机制,我们就无法将该函数用于断言。是的,我们可以在我们的具体指标中定义 ShortName(例如使用全局变量,很多人都这么做,尽管这是个坏习惯),但我们没有通用函数 "GetShortName()"。因此,我们无法用 ChartIndicatorDelete() 制作通用机制(我指的是适用于所有指标的宏或函数,只需添加一行 "assert(...) "即可)。

问题出在哪里?你正在处理你的指标,这是你的代码,所以你知道它的简短名称。

我发帖是想说,你说没有办法立即终止程序是不对的。你应该为你的断言项目找到解决方案。

在指示器中使用全局变量绝对不是一个坏习惯。当然,如果你想用 "这是不好的做法 "这样的断言来创造自己的新限制,你会发现很多不可能的事情。

请向我展示脚本中任何代码部分的 "琐碎 "变体。脚本的任何部分都必须是一个(!)函数或宏:

1) 对于 cicles
2) 对于任何返回类型的函数
3) 对于无返回类型(void)的函数。

因此,我们必须在脚本的任何部分添加一行 "assert(...)",就像这样:

与 EA 相同。

Sergey Eremin
Sergey Eremin | 6 12月 2015 在 12:30
Alain Verleyen:

我发帖是想说,你说没有办法立即终止程序是不对的。你应该为你的 Assertion 项目找到解决方案。

在指标中使用全局变量绝对不是一个坏习惯。当然,如果你想用 "这是不好的做法 "这样的断言来创建自己的新限制,你会发现很多不可能的事情。

好的,我明白了。谢谢,你说得对。

但在我的文章中,我指的是断言的解决方案:在代码的任何地方(包括 OnInit 和 cicles)停止 MQL4/5 应用程序的通用 机制。只需在任何 部分添加一行 即可完成。就像在许多编程语言中的任何断言机制中一样;)

是的,你的变体是正确的。但不适合我对断言 的理解,因为它不是针对任何 代码的任何部分 的通用解决方案。

谢谢你的 EA 例子。

Alain Verleyen
Alain Verleyen | 6 12月 2015 在 13:20
Sergey Eremin:

好的,我明白了。谢谢,你说得对。

但在我的文章中,我指的是断言的解决方案:在代码的任何地方(包括 OnInit 和 cicles)停止 MQL4/5 应用程序的通用 机制。只需在任何 部分添加一行 即可完成。就像在许多编程语言中的任何断言机制中一样;)

是的,你的变体是正确的。但不适合我对断言 的理解,因为它不是针对任何 代码的任何部分 的通用解决方案。

谢谢你的 EA 例子。

我知道你想做什么,而且完全可以做到,你只需根据我提供的代码进行归纳即可。

通过分析宏中的调用上下文,检测它是 EA 还是指标,并解析__FUNCSIG__。

能否将其变成一种通用机制取决于您。

Sergey Eremin
Sergey Eremin | 6 12月 2015 在 13:36
Alain Verleyen:

我知道你想做什么,而且完全可以做到,你只需根据我提供的代码进行归纳。

通过分析宏中的调用上下文,检测它是 EA 还是指标,并解析 __FUNCSIG__。

能否将其变成一种通用机制取决于您。

是的,起初我也考虑过这样的问题,但最终我做到了我们在文章中看到的那样:)

感谢您的评论!

mktr8591
mktr8591 | 13 4月 2021 在 19:38

如果有人要使用这段代码,请记住这一点:下面的脚本

   if(true)
      assert(1==1, "")
   else
      Print("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(...) 后面加上";")。

测试可视化: 功能增强 测试可视化: 功能增强
本文描述了能够使策略测试与真实交易非常接近的软件.
怎样使用崩溃记录来调试您的动态链接库(DLL) 怎样使用崩溃记录来调试您的动态链接库(DLL)
在收到的用户崩溃记录中,有25%到30%是因为执行自定义动态链接库(DLL)中的输入函数而出的错.
交易新手的十个"错误"? 交易新手的十个"错误"?
本文证实了, 构造一个随意的交易系统, 它只是进行一系列的建仓和平仓而不论现实情况如何 - 价格以及当前每个订单的盈利/亏损, 而它和传统的"提醒"交易系统结果差别并不大. 我们会给出一个这样基本交易系统的典型实现.
MQL4 作为交易者的工具, 还是高级技术分析 MQL4 作为交易者的工具, 还是高级技术分析
交易首先是对可能性的计算. 有一句谚语, 懒惰是进步的引擎, 这也揭示了指标以及交易系统被开发出来的原因. 绝大多数交易新手学习的都是"成型"的交易理论. 但是, 如果够幸运的话, 还有更多的没有被发现的市场奥秘和用于分析价格走向的工具, 例如那些还没有实现的技术指标或者数学和统计学工具包. 非常感谢比尔.威廉姆斯对市场运行理论的贡献. 虽然,也许现在休息是太早了些.