实数的正常性检验

由于实数计算可能出现异常情况,诸如超出函数范围、获得数字无穷大、丢失顺序等等,导致结果可能不包含数字。相反,结果可能包含特殊值,该特殊值实际上描述了问题的性质。所有这些特殊值具有一个通用名称:“非数字”(简称 NaN)。

我们在本书前面的章节中已接触了这种值。尤其是在输出到日志时(参见章节 数字转换为字符串以及相反转换),它们显示为文本标签(如 nan(ind)+inf 等等)。另一个特征是,任何表达式的操作数中只要存在一个 NaN 值,整个表达式就会停止正确计算,开始给出 NaN 结果。唯一例外是表示正负无穷“非数字”:如果用其他数除以正负无穷,结果为零。然而有一个预期例外:如果我们将无穷除以无穷,我们又得到 NaN。

因此,重要的是让程序确定计算中出现 NaN 的时刻,并以特殊方式处理该情况:提示错误,替换某些可接受默认值,或者以其它参数重复计算(例如,减少迭代算法的精度/步骤)。

在 MQL5 中有 2 个函数可以分析实数的正常性:MathIsValidNumber 给出一个简单的回答:是 (true) 或否 (false),而 MathClassify 生成更详细的类别划分。

在物理层面,所有特殊值都通过位的特殊组合编码在数字中,这些位组合不用于表示普通数字。对于 doublefloat 类型,这些编码当然不同。我们来深入了解一下 double(因为它比 float 更常用)。

嵌套模板一章中,我们创建了 Converter 类,用于通过在一个联合体中组合两个不同的类型来切换视图。我们将该类用于研究 NaN 位设备。

出于方便,我们将把该类移动到一个单独的头文件 ConverterT.mqh。我们在测试脚本 MathInvalid.mq5 中连接该 mqh 文件,并为一系列类型 double/ulong 创建一个转换器实例(顺序不重要,因为该转换器能够双向工作)。

static Converter<ulongdoubleNaNs;

NaN 中的位组合经过标准化,因此我们取几个由常量 ulong 表示的常用值,看看内置函数如何应对。

// basic NaNs
#define NAN_INF_PLUS  0x7FF0000000000000
#define NAN_INF_MINUS 0xFFF0000000000000
#define NAN_QUIET     0x7FF8000000000000
#define NAN_IND_MINUS 0xFFF8000000000000
   
// custom NaN examples
#define NAN_QUIET_1   0x7FF8000000000001
#define NAN_QUIET_2   0x7FF8000000000002
   
static double pinf = NaNs[NAN_INF_PLUS];  // +infinity
static double ninf = NaNs[NAN_INF_MINUS]; // -infinity
static double qnan = NaNs[NAN_QUIET];     // quiet NaN
static double nind = NaNs[NAN_IND_MINUS]; // -nan(ind)
   
void OnStart()
{
   PRT(MathIsValidNumber(pinf));               // false
   PRT(EnumToString(MathClassify(pinf)));      // FP_INFINITE
   PRT(MathIsValidNumber(nind));               // false
   PRT(EnumToString(MathClassify(nind)));      // FP_NAN
   ...
}

正如所预期的那样,结果是相同的。

我们看看 MathIsValidNumberMathClassify 函数的正式说明,然后继续测试。

bool MathIsValidNumber(double value)

该函数检查实数的正确性。参数可以是 doublefloat 类型。结果 true 表示数字正确,false 表示“非数字”(某种 NaN)。

ENUM_FP_CLASS MathClassify(double value)

该函数返回实数的类别(doublefloat 类型),其是 ENUM_FP_CLASS 枚举值之一:

  • FP_NORMAL 是一个正规数。
  • FP_SUBNORMAL 是一个小于以正规化形式可表示的最小数字的数字(例如,对于 double 类型,这些是小于 DBL_MIN, 2.2250738585072014e-308 的值);顺序(精度)丢失。
  • FP_ZERO 为零(正或负)。
  • FP_INFINITE 为无穷大(正或负)。
  • FP_NAN 指所有其它类型的“非数字”(细分为“静默型”和“信号型”NaN 系列)。

MQL5 不提供警报型 NaN,警报型 NaN 用于异常机制中,允许截获并回应程序内的关键错误。在 MQL5 中没有此类机制,因此,例如,如果出现除零情况,MQL 程序会直接终止其工作(从图表卸载)。

可能有很多“静默型”NaN,你可以使用转换器构造,以区分和处理计算算法中的非标准状态。

我们在 MathInvalid.mq5 中执行某些计算,以直观展示不同类别的数字的获得方式。

 // calculations with double
   PRT(MathIsValidNumber(0));                      // true
   PRT(EnumToString(MathClassify(0)));             // FP_ZERO
   PRT(MathIsValidNumber(M_PI));                   // true
   PRT(EnumToString(MathClassify(M_PI)));          // FP_NORMAL
   PRT(DBL_MIN / 10);                              // 2.225073858507203e-309
   PRT(MathIsValidNumber(DBL_MIN / 10));           // true
   PRT(EnumToString(MathClassify(DBL_MIN / 10)));  // FP_SUBNORMAL
   PRT(MathSqrt(-1.0));                            // -nan(ind)
   PRT(MathIsValidNumber(MathSqrt(-1.0)));         // false
   PRT(EnumToString(MathClassify(MathSqrt(-1.0))));// FP_NAN
   PRT(MathLog(0));                                // -inf
   PRT(MathIsValidNumber(MathLog(0)));             // false
   PRT(EnumToString(MathClassify(MathLog(0))));    // FP_INFINITE
   
 // calculations with float
   PRT(1.0f / FLT_MIN / FLT_MIN);                             // inf
   PRT(MathIsValidNumber(1.0f / FLT_MIN / FLT_MIN));          // false
   PRT(EnumToString(MathClassify(1.0f / FLT_MIN / FLT_MIN))); // FP_INFINITE

我们可以在相反方向使用转换器:通过 double 值获取其位表示,进而探测“非数字”:

   PrintFormat("%I64X"NaNs[MathSqrt(-1.0)]);      // FFF8000000000000
   PRT(NaNs[MathSqrt(-1.0)] == NAN_IND_MINUS);      // true, nind

PrintFormat 函数类似于 StringFormat;唯一的不同是结果立即打印到日志,而不是打印到字符串。

最后,我们确保“非数字”始终不相等:

   // NaN != NaN always true
   PRT(MathSqrt(-1.0) != MathSqrt(-1.0)); // true
   PRT(MathSqrt(-1.0) == MathSqrt(-1.0)); // false

要在 MQL5 中获取 NaN 或无穷大,有一种基于将字符串 "nan" 和 "inf" 转化为 double 的方法。

double nan = (double)"nan";
double infinity = (double)"inf";