外汇EA编写教程:在MQL5程序中使用断言

简介

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

例如,如果预期特定的值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 Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/1977

附加的文件 |

assert.mqh
(2.03 KB)

 

 


MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投