Пользовательские ошибки

Разработчик может использовать встроенную переменную _LastError в собственных, прикладных целях. Это позволяет сделать функция SetUserError.

void SetUserError(ushort user_error)

Функция устанавливает встроенную переменную _LastError в значение ERR_USER_ERROR_FIRST + user_error, где ERR_USER_ERROR_FIRST равно 65536. Все коды ниже этой величины зарезервированы за системными ошибками.

С помощью данного механизма можно отчасти обойти ограничение MQL5, связанное с тем, что в языке не поддерживаются исключения.

Довольно часто функции используют возвращаемое значение как признак ошибки. Однако бывают алгоритмы, в которых функция должна возвращать величину прикладного типа. Допустим речь о double. Если функция имеет область определения от минус до плюс бесконечности, любое значение, выбранное нами для индикации ошибки (например, 0), будет невозможно отличить от реального результата вычисления. В случае double, конечно, есть вариант вернуть специально сконструированное значение NaN (Not a Number, см. раздел Проверка вещественных чисел на нормальность). Но что если функция возвращает структуру или объект класса? Одно из решений — возвращать результат через параметр по ссылке или указателю, но такая форма делает невозможным использование функций в качестве операндов выражений.  

В контексте разговора о классах уместно вспомнить об особых функциях — конструкторах. Они обязаны вернуть новый экземпляр объекта. Однако иногда обстоятельства мешают сконструировать объект полностью, и тогда вызывающий код вроде бы и получает объект, но не должен им пользоваться. Хорошо, если в классе можно предусмотреть дополнительный метод, который позволил бы проверить полноценность объекта. Но в качестве единообразного альтернативного подхода (например, охватывающего все классы) можно задействовать SetUserError.

В разделе Перегрузка операторов мы познакомились с классом Matrix. Дополним его методами расчета определителя и обратной матрицы, а затем используем для демонстрации пользовательских ошибок (см. файл Matrix.mqh). Напомним, что для матриц были определены перегруженные операторы, позволяющие составлять их в цепочки операторов, в едином выражении, и потому внедрять в него проверку на потенциальные ошибки было бы неудобно.

Наш класс Matrix является собственной альтернативной реализацией для недавно добавленного в MQL5 встроенного объектного типа matrix.

Прежде всего, в конструкторах основного класса 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();
   MatrixSquare mA(a);   // присваиваем данные в исходную матрицу
   Print("Input");
   mA.print();
   MatrixSquare mAinv(3);
   mAinv = !mA;          // инвертируем и сохраняем в другой матрице
   Print("Result");
   mAinv.print();
   
   Print("Check inverted by multiplication");
   MatrixSquare test(3); // умножаем первую на вторую
   test = mA * mAinv;
   test.print();         // получаем единичную матрицу
   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

В этих случаях мы не можем не создать объект, поскольку компилятор делает это автоматически.

Конечно, в данном тесте очевидны нарушения контрактов (спецификаций того, что классы и методы "считают" корректными данными и действиями). Однако на практике аргументы часто получаются из других частей кода, в ходе обработки объемных, "чужих" данных, и обнаружить отклонения от ожиданий — не такая уж тривиальная задача.

Способность программы "переваривать" некорректные данные без фатальных последствий является важнейшим показателем её качества, наравне с выдачей правильных результатов для правильных входных данных.