用户定义的错误
开发人员可将内置的 _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 r, const int c) : rows(r), columns(c)
{
if(rows <= 0 || columns <= 0)
{
SetUserError(ERR_USER_MATRIX_EMPTY);
}
else
{
ArrayResize(m, rows * columns);
ArrayInitialize(m, 0);
}
}
|
这些新操作仅为方矩阵定义,因此,我们创建一个具有适当大小约束的派生类。
class MatrixSquare : public Matrix
{
public:
MatrixSquare(const int n, const int _ = -1) : Matrix(n, n)
{
if(_ != -1 && _ != n)
{
SetUserError(ERR_USER_MATRIX_NOT_SQUARE);
}
}
...
|
构造函数中的第二个参数应空缺(假定它等于第一个),但是我们需要它,因为 Matrix 类具有一种模板换置方法,其中所有 T 类型必须支持一个具有两个整数参数的构造函数。
class Matrix
{
...
template<typename T>
T transpose() const
{
T result(columns, rows);
for(int i = 0; i < rows; ++i)
{
for(int j = 0; j < 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, -7, 17,
-21, 15, 9,
-34,-31, 33
};
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(1, 2);
Print(EnumToString(Matrix::lastError()));
}
|
这里我们描述一个空矩阵和一个假定方矩阵(但大小不同)。
Empty matrix creation
ERR_USER_MATRIX_EMPTY
'Rectangular' square matrix creation
ERR_USER_MATRIX_NOT_SQUARE
|
在这些情况下,我们无法避免地要创建对象,因为编译器会自动创建。
当然,此测试显然违反约定(数据和操作规范中关于类和方法“视为”有效的约定)。然而,在实践中,自变量通常是从代码的其它部分获取的,在处理大型“第三方”数据的过程中,检测与期望的偏差并非易事。
程序能否“消化”不正确数据而不引发致命后果,这一点与为正确输入数据生成正确结果一样,都是程序质量最重要的指标。