用户定义的错误

开发人员可将内置的 _LastError 变量用于他们自身的应用目的。SetUserError 函数为这种应用提供了便利。

void SetUserError(ushort user_error)

该函数将内置 _LastError 变量设为 ERR_USER_ERROR_FIRST + user_error 值,其中 ERR_USER_ERROR_FIRST 为 65536。所有低于该值的代码均为系统错误预留。

MQL5 中不支持异常,但利用上述机制,你可以在一定程度上绕过这种局限性。

很多时候,函数使用返回值作为错误标志。然而,在某些算法中,函数必须返回应用程序类型值。我们以 double 为例。如果该函数的定义范围是从负到正无穷,则我们选择用于指示错误的任何值(例如 0)将与实际计算结果混淆。当然,对于 double 来说,有一个返回特别构造的 NaN 值的选项(NaN 表示非数字,参见章节 检查实数正常性)。但如果该函数返回一个结构体或一个类对象怎么办?一种可能的解决方案是通过一个参数按引用或按指针返回结果,但这种方式无法将函数用作表达式的操作数。

在类上下文中,我们来了解名为“构造函数”的特殊函数。构造函数可返回对象的新实例。然而,有时候情况不允许你构造整个对象,在此情况下,调用代码看似获得了对象但却无法使用。如果类能够提供一种额外的方法来检查对象是否可用,这当然不错。但是作为一种统一的替代方法(例如,涵盖所有类),我们可以使用 SetUserError

运算符重载 一节,我们认识了 Matrix 类。我们将为其补充用于计算行列式和逆矩阵的方法,然后将其用于演示用户错误(参见文件 Matrix.mqh)。为矩阵定义了重载运算符,允许它们组合到一个表达式中的运算符链中,因此在其中实现潜在错误检查就不是很方便。

我们的 Matrix 类是对最近添加的 MQL5 内置对象类型 矩阵的自定义替代实现。

我们首先验证 Matrix 主类构造函数中的输入参数。如果有人试图创建一个零大小的矩阵,我们设置一个自定义错误 ERR_USER_MATRIX_EMPTY(提供的若干自定义错误之一)。

enum ENUM_ERR_USER_MATRIX
{
   ERR_USER_MATRIX_OK = 0
   ERR_USER_MATRIX_EMPTY =  1
   ERR_USER_MATRIX_SINGULAR = 2
   ERR_USER_MATRIX_NOT_SQUARE = 3
};
   
class Matrix
{
   ...
public:
   Matrix(const int rconst int c) : rows(r), columns(c)
   {
      if(rows <= 0 || columns <= 0)
      {
         SetUserError(ERR_USER_MATRIX_EMPTY);
      }
      else
      {
         ArrayResize(mrows * columns);
         ArrayInitialize(m0);
      }
   }

这些新操作仅为方矩阵定义,因此,我们创建一个具有适当大小约束的派生类。

class MatrixSquare : public Matrix
{
public:
   MatrixSquare(const int nconst int _ = -1) : Matrix(nn)
   {
      if(_ != -1 && _ != n)
      {
         SetUserError(ERR_USER_MATRIX_NOT_SQUARE);
      }
   }
   ...

构造函数中的第二个参数应空缺(假定它等于第一个),但是我们需要它,因为 Matrix 类具有一种模板换置方法,其中所有 T 类型必须支持一个具有两个整数参数的构造函数。

class Matrix
{
   ...
   template<typename T>
   T transpose() const
   {
      T result(columnsrows);
      for(int i = 0i < rows; ++i)
      {
         for(int j = 0j < columns; ++j)
         {
            result[j][i] = this[i][(uint)j];
         }
      }
      return result;
   }

由于在 MatrixSquare 函数中有两个参数,我们还必须检查它们的强制相等性。如果它们不相等,则我们设置 ERR_USER_MATRIX_NOT_SQUARE 错误。

最后,在逆矩阵计算期间,我们会发现矩阵退化(行列式为 0)。预留错误 ERR_USER_MATRIX_SINGULAR 便是用于这种情况。

class MatrixSquare : public Matrix
{
public:
   ...
   MatrixSquare inverse() const
   {
      MatrixSquare result(rows);
      const double d = determinant();
      if(fabs(d) > DBL_EPSILON)
      {
         result = complement().transpose<MatrixSquare>() * (1 / d);
      }
      else
      {
         SetUserError(ERR_USER_MATRIX_SINGULAR);
      }
      return result;
   }
   
   MatrixSquare operator!() const
   {
      return inverse();
   }
   ...

对于可视化错误输出,必须向日志添加静态方法,返回 ENUM_ERR_USER_MATRIX 枚举,可将其轻松传给 EnumToString

   static ENUM_ERR_USER_MATRIX lastError()
   {
      if(_LastError >= ERR_USER_ERROR_FIRST)
      {
         return (ENUM_ERR_USER_MATRIX)(_LastError - ERR_USER_ERROR_FIRST);
      }
      return (ENUM_ERR_USER_MATRIX)_LastError;
   }

所有方法的完整代码参见随附文件。

我们将在 EnvError.mq5 测试脚本中检查应用程序错误代码。

首先,我们确保该类可正常工作:逆反矩阵,确保原始矩阵和逆矩阵的乘积等于单位矩阵。

void OnStart()
{
   Print("Test matrix inversion (should pass)");
   double a[9] =
   {
      1,  2,  3
      4,  5,  6
      7,  8,  0
   };
      
   ResetLastError();
   Matrix SquaremA(a);   // assign data to the original matrix
   Print("Input");
   mA.print();
   MatrixSquare mAinv(3);
   mainv = !mA;          // invert and store in another matrix
   Print("Result");
   mAinv.print();
   
   Print("Check inverted by multiplication");
   Matrix Squaretest(3); // multiply the first by the second
   test = mA * mAinv;
   test.print();         // get identity matrix
   Print(EnumToString(Matrix::lastError())); // ok
   ...

该代码片断生成以下日志条目。

Test matrix inversion (should pass)
Input
1.00000 2.00000 3.00000
4.00000 5.00000 6.00000
7.00000 8.00000 0.00000
Result
-1.77778  0.88889 -0.11111
 1.55556 -0.77778  0.22222
-0.11111  0.22222 -0.11111
Check inverted by multiplication
 1.00000 +0.00000  0.00000
 -0.00000   1.00000  +0.00000
0.00000 0.00000 1.00000
ERR_USER_MATRIX_OK

注意在单位矩阵中,由于浮点错误,一些零元素实际上是接近零的极小值,因此它们有符号。

然后,我们看看算法如何处理退化矩阵。

   Print("Test matrix inversion (should fail)");
   double b[9] =
   {
     -22, -717
     -2115,  9
     -34,-3133
   };
   
   MatrixSquare mB(b);
   Print("Input");
   mB.print();
   ResetLastError();
   Print("Result");
   (!mB).print();
   Print(EnumToString(Matrix::lastError())); // singular
   ...

结果呈现如下。

Test matrix inversion (should fail)
Input
-22.00000  -7.00000  17.00000
-21.00000  15.00000   9.00000
-34.00000 -31.00000  33.00000
Result
0.0 0.0 0.0
0.0 0.0 0.0
0.0 0.0 0.0
ERR_USER_MATRIX_SINGULAR

在本例中,我们只是显示了错误说明。但在真实程序中,应能够选择一个继续选项,取决于问题的性质。

最后,我们将模拟两个剩余应用错误的情况。

   Print("Empty matrix creation");
   MatrixSquare m0(0);
   Print(EnumToString(Matrix::lastError()));
   
   Print("'Rectangular' square matrix creation");
   MatrixSquare r12(12);
   Print(EnumToString(Matrix::lastError()));
}

这里我们描述一个空矩阵和一个假定方矩阵(但大小不同)。

Empty matrix creation
ERR_USER_MATRIX_EMPTY
'Rectangular' square matrix creation
ERR_USER_MATRIX_NOT_SQUARE

在这些情况下,我们无法避免地要创建对象,因为编译器会自动创建。

当然,此测试显然违反约定(数据和操作规范中关于类和方法“视为”有效的约定)。然而,在实践中,自变量通常是从代码的其它部分获取的,在处理大型“第三方”数据的过程中,检测与期望的偏差并非易事。

程序能否“消化”不正确数据而不引发致命后果,这一点与为正确输入数据生成正确结果一样,都是程序质量最重要的指标。