Lỗi do người dùng định nghĩa
Nhà phát triển có thể sử dụng biến tích hợp _LastError
cho các mục đích ứng dụng của riêng họ. Điều này được hỗ trợ bởi hàm SetUserError
.
void SetUserError(ushort user_error)
Hàm này đặt biến tích hợp _LastError
thành giá trị ERR_USER_ERROR_FIRST + user_error
, trong đó ERR_USER_ERROR_FIRST
là 65536. Tất cả các mã dưới giá trị này được dành sẵn cho các lỗi hệ thống.
Bằng cách sử dụng cơ chế này, bạn có thể phần nào vượt qua hạn chế của MQL5 liên quan đến việc ngôn ngữ không hỗ trợ ngoại lệ (exceptions).
Thông thường, các hàm sử dụng giá trị trả về như một dấu hiệu của lỗi. Tuy nhiên, có những thuật toán mà hàm phải trả về một giá trị thuộc kiểu ứng dụng. Hãy nói về double
. Nếu hàm có phạm vi định nghĩa từ âm vô cực đến dương vô cực, bất kỳ giá trị nào chúng ta chọn để biểu thị lỗi (ví dụ: 0) sẽ không thể phân biệt được với kết quả thực tế của phép tính. Trong trường hợp của double
, tất nhiên, có một lựa chọn là trả về giá trị NaN (Not a Number, xem phần Kiểm tra số thực có bình thường không). Nhưng nếu hàm trả về một cấu trúc hoặc đối tượng lớp thì sao? Một trong những giải pháp khả thi là trả về kết quả qua tham số theo tham chiếu hoặc con trỏ, nhưng dạng này khiến không thể sử dụng hàm như toán hạng trong biểu thức.
Trong bối cảnh của các lớp, hãy xem xét các hàm đặc biệt được gọi là "hàm khởi tạo" (constructors). Chúng trả về một phiên bản mới của đối tượng. Tuy nhiên, đôi khi hoàn cảnh ngăn cản việc xây dựng toàn bộ đối tượng, và khi đó mã gọi dường như nhận được đối tượng nhưng không nên sử dụng nó. Sẽ tốt nếu lớp có thể cung cấp một phương thức bổ sung cho phép kiểm tra tính hữu dụng của đối tượng. Nhưng như một cách tiếp cận thay thế đồng nhất (ví dụ, áp dụng cho tất cả các lớp), chúng ta có thể sử dụng SetUserError
.
Trong phần Nạp chồng toán tử, chúng ta đã gặp lớp Matrix
. Chúng ta sẽ bổ sung cho nó các phương thức để tính định thức và ma trận nghịch đảo, sau đó sử dụng nó để thể hiện các lỗi do người dùng định nghĩa (xem tệp Matrix.mqh
). Các toán tử nạp chồng đã được định nghĩa cho ma trận, cho phép kết hợp chúng thành chuỗi toán tử trong một biểu thức duy nhất, do đó việc triển khai kiểm tra lỗi tiềm ẩn trong đó sẽ bất tiện.
Lớp
Matrix
của chúng ta là một triển khai thay thế tùy chỉnh cho kiểu đối tượng tích hợp mới được thêm vào MQL5 là matrix.
Chúng ta bắt đầu bằng cách xác thực các tham số đầu vào trong các hàm khởi tạo chính của lớp Matrix
. Nếu ai đó cố gắng tạo một ma trận có kích thước bằng 0, hãy đặt lỗi tùy chỉnh ERR_USER_MATRIX_EMPTY
(một trong số các lỗi được cung cấp).
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Các phép toán mới này chỉ được định nghĩa cho ma trận vuông, vì vậy hãy tạo một lớp dẫn xuất với ràng buộc kích thước phù hợp.
class MatrixSquare : public Matrix
{
public:
MatrixSquare(const int n, const int _ = -1) : Matrix(n, n)
{
if(_ != -1 && _ != n)
{
SetUserError(ERR_USER_MATRIX_NOT_SQUARE);
}
}
...
2
3
4
5
6
7
8
9
10
11
Tham số thứ hai trong hàm khởi tạo lẽ ra không nên có (giả định nó bằng tham số đầu tiên), nhưng chúng ta cần nó vì lớp Matrix
có một phương thức chuyển vị mẫu, trong đó tất cả các kiểu T phải hỗ trợ hàm khởi tạo với hai tham số nguyên.
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Do thực tế là có hai tham số trong hàm khởi tạo MatrixSquare
, chúng ta cũng phải kiểm tra tính bắt buộc bằng nhau của chúng. Nếu chúng không bằng nhau, chúng ta đặt lỗi ERR_USER_MATRIX_NOT_SQUARE
.
Cuối cùng, trong quá trình tính ma trận nghịch đảo, chúng ta có thể phát hiện ra rằng ma trận bị suy biến (định thức bằng 0). Lỗi ERR_USER_MATRIX_SINGULAR
được dành sẵn cho trường hợp này.
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();
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Để hiển thị lỗi một cách trực quan, một phương thức tĩnh đã được thêm vào để ghi nhật ký, trả về liệt kê ENUM_ERR_USER_MATRIX
, dễ dàng truyền vào 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;
}
2
3
4
5
6
7
8
Mã đầy đủ của tất cả các phương thức có thể được tìm thấy trong tệp đính kèm.
Chúng ta sẽ kiểm tra mã lỗi ứng dụng trong script thử nghiệm EnvError.mq5
.
Đầu tiên, hãy đảm bảo rằng lớp hoạt động: nghịch đảo ma trận và kiểm tra rằng tích của ma trận ban đầu và ma trận nghịch đảo bằng ma trận đơn vị.
void OnStart()
{
Print("Test matrix inversion (should pass)");
double a[9] =
{
1, 2, 3,
4, 5, 6,
7, 8, 0,
};
ResetLastError();
MatrixSquare mA(a); // gán dữ liệu vào ma trận gốc
Print("Input");
mA.print();
MatrixSquare mAinv(3);
mAinv = !mA; // nghịch đảo và lưu vào ma trận khác
Print("Result");
mAinv.print();
Print("Check inverted by multiplication");
MatrixSquare test(3); // nhân ma trận thứ nhất với ma trận thứ hai
test = mA * mAinv;
test.print(); // nhận ma trận đơn vị
Print(EnumToString(Matrix::lastError())); // ok
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Đoạn mã này tạo ra các mục nhật ký sau.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
Lưu ý rằng trong ma trận đơn vị, do lỗi số thực dấu phẩy động, một số phần tử bằng 0 thực tế là các giá trị rất nhỏ gần 0, và do đó chúng có dấu.
Tiếp theo, hãy xem cách thuật toán xử lý ma trận suy biến.
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
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Kết quả được trình bày dưới đây.
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
2
3
4
5
6
7
8
9
10
Trong trường hợp này, chúng ta chỉ hiển thị mô tả lỗi. Nhưng trong một chương trình thực tế, cần có khả năng chọn tùy chọn tiếp tục, tùy thuộc vào bản chất của vấn đề.
Cuối cùng, chúng ta sẽ mô phỏng các tình huống cho hai lỗi ứng dụng còn lại.
Print("Empty matrix creation");
MatrixSquare m0(0);
Print(EnumToString(Matrix::lastError()));
Print("'Rectangular' square matrix creation");
MatrixSquare r12(1, 2);
Print(EnumToString(Matrix::lastError()));
}
2
3
4
5
6
7
8
Ở đây chúng ta mô tả một ma trận rỗng và một ma trận được cho là vuông nhưng có kích thước khác nhau.
Empty matrix creation
ERR_USER_MATRIX_EMPTY
'Rectangular' square matrix creation
ERR_USER_MATRIX_NOT_SQUARE
2
3
4
Trong những trường hợp này, chúng ta không thể tránh việc tạo đối tượng vì trình biên dịch thực hiện điều này tự động.
Tất nhiên, bài kiểm tra này rõ ràng vi phạm các hợp đồng (thông số kỹ thuật của dữ liệu và hành động mà các lớp và phương thức "coi" là hợp lệ). Tuy nhiên, trong thực tế, các đối số thường được lấy từ các phần khác của mã, trong quá trình xử lý dữ liệu lớn từ "bên thứ ba", và việc phát hiện sai lệch so với kỳ vọng không hề dễ dàng.
Khả năng của một chương trình để "tiêu hóa" dữ liệu không chính xác mà không gây hậu quả nghiêm trọng là chỉ số quan trọng nhất về chất lượng của nó, cùng với việc tạo ra kết quả đúng cho dữ liệu đầu vào chính xác.