Nạp chồng toán tử
Trong chương Biểu thức, chúng ta đã tìm hiểu về các phép toán khác nhau được định nghĩa cho các kiểu tích hợp sẵn. Ví dụ, với các biến kiểu double
, chúng ta có thể đánh giá biểu thức sau:
double a = 2.0, b = 3.0, c = 5.0;
double d = a * b + c;
2
Sẽ rất tiện lợi nếu sử dụng cú pháp tương tự khi làm việc với các kiểu do người dùng định nghĩa, chẳng hạn như ma trận:
Matrix a(3, 3), b(3, 3), c(3, 3); // tạo ma trận 3x3
// ... каким-то образом заполнить a, b, c
Matrix d = a * b + c;
2
3
MQL5 cung cấp khả năng này nhờ vào việc nạp chồng toán tử.
Kỹ thuật này được tổ chức bằng cách mô tả các phương thức với tên bắt đầu bằng từ khóa operator
, sau đó chứa ký hiệu (hoặc chuỗi ký hiệu) của một trong những phép toán được hỗ trợ. Ở dạng tổng quát, điều này có thể được biểu diễn như sau:
result_type operator@ ([type parameter_name]);
Ở đây, @ là ký hiệu của phép toán.
Danh sách đầy đủ các phép toán của MQL5 đã được cung cấp trong phần Ưu tiên phép toán, tuy nhiên, không phải tất cả đều được phép nạp chồng.
Các phép toán bị cấm nạp chồng:
- Dấu hai chấm
::
, quyền truy cập ngữ cảnh; - Dấu ngoặc đơn
()
, "gọi hàm" hoặc "nhóm"; - Dấu chấm
.
, "giải tham chiếu"; - Dấu và
&
, "lấy địa chỉ", toán tử đơn (tuy nhiên, dấu và có thể dùng như toán tử nhị phân "AND bitwise"); - Toán tử điều kiện ba ngôi
?:
; - Dấu phẩy
,
.
Tất cả các toán tử khác đều có thể được nạp chồng. Độ ưu tiên của toán tử nạp chồng không thể thay đổi, chúng giữ nguyên thứ tự ưu tiên tiêu chuẩn, vì vậy cần sử dụng dấu ngoặc để nhóm nếu cần thiết.
Bạn không thể tạo nạp chồng cho một ký tự mới nào không nằm trong danh sách tiêu chuẩn.
Tất cả các toán tử được nạp chồng dựa trên tính đơn/nhị phân của chúng, nghĩa là số lượng toán hạng yêu cầu được giữ nguyên. Như bất kỳ phương thức lớp nào, nạp chồng toán tử có thể trả về giá trị của một kiểu nào đó. Trong trường hợp này, kiểu trả về nên được chọn dựa trên logic dự kiến khi sử dụng kết quả của hàm trong các biểu thức (xem thêm ở phần sau).
Các phương thức nạp chồng toán tử có dạng sau (thay vì ký hiệu '@', ký hiệu của toán tử cần thiết được thay thế):
Tên | Tiêu đề phương thức | Sử dụng trong biểu thức | Tương đương với hàm |
---|---|---|---|
Đơn tiền tố | type operator@() | @object | object.operator@() |
Đơn hậu tố | type operator@(int) | object@ | object.operator@(0) |
Nhị phân | type operator@(type parameter_name) | object@argument | object.operator@(argument) |
Chỉ số | type operator[](type index_name) | object[argument] | object.operator[](argument) |
Các toán tử đơn không nhận tham số. Trong số các toán tử đơn, chỉ các toán tử tăng ++
và giảm --
hỗ trợ dạng hậu tố ngoài dạng tiền tố, tất cả các toán tử đơn khác chỉ hỗ trợ dạng tiền tố. Việc chỉ định một tham số vô danh kiểu int
được dùng để biểu thị dạng hậu tố (để phân biệt với dạng tiền tố), nhưng tham số này bị bỏ qua.
Các toán tử nhị phân phải nhận một tham số. Đối với cùng một toán tử, có thể có nhiều biến thể nạp chồng với tham số thuộc kiểu khác nhau, bao gồm cùng kiểu với lớp của đối tượng hiện tại. Trong trường hợp này, các đối tượng làm tham số chỉ có thể được truyền bằng tham chiếu hoặc bằng con trỏ (trường hợp sau chỉ áp dụng cho đối tượng lớp, không phải cấu trúc).
Các toán tử đã nạp chồng có thể được sử dụng cả qua cú pháp của phép toán trong biểu thức (lý do chính để nạp chồng) và cú pháp gọi phương thức; cả hai tùy chọn được hiển thị trong bảng trên. Phần tương đương hàm làm rõ rằng, về mặt kỹ thuật, một toán tử không gì khác ngoài một lời gọi phương thức trên một đối tượng, với đối tượng nằm bên phải của toán tử tiền tố và bên trái của ký hiệu cho tất cả các trường hợp khác. Phương thức toán tử nhị phân sẽ nhận giá trị hoặc biểu thức ở bên phải của toán tử làm đối số (điều này có thể là một đối tượng khác hoặc biến kiểu tích hợp).
Từ đó suy ra rằng các toán tử đã nạp chồng không có tính chất giao hoán: a@b
nói chung không bằng b@a
, vì đối với a
, toán tử @ có thể được nạp chồng, nhưng đối với b
thì không. Hơn nữa, nếu b
là biến hoặc giá trị của kiểu tích hợp, thì về nguyên tắc, bạn không thể nạp chồng hành vi tiêu chuẩn cho nó.
Là ví dụ đầu tiên, hãy xem xét lớp Fibo
để tạo các số từ dãy Fibonacci (chúng ta đã thực hiện một triển khai của bài toán này bằng hàm, xem Định nghĩa hàm). Trong lớp, chúng ta sẽ cung cấp 2 trường để lưu trữ số hiện tại và số trước đó trong dãy: current
và previous
, tương ứng. Hàm tạo mặc định sẽ khởi tạo chúng với giá trị 1 và 0. Chúng ta cũng sẽ cung cấp một hàm tạo sao chép (FiboMonad.mq5
).
class Fibo
{
int previous;
int current;
public:
Fibo() : current(1), previous(0) { }
Fibo(const Fibo &other) : current(other.current), previous(other.previous) { }
...
};
2
3
4
5
6
7
8
9
Trạng thái ban đầu của đối tượng: số hiện tại là 1, và số trước đó là 0. Để tìm số tiếp theo trong dãy, chúng ta nạp chồng toán tử tăng tiền tố và hậu tố.
Fibo *operator++() // tiền tố
{
int temp = current;
current = current + previous;
previous = temp;
return &this;
}
Fibo operator++(int) // hậu tố
{
Fibo temp = this;
++this;
return temp;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Lưu ý rằng phương thức tiền tố không trả về con trỏ tới đối tượng Fibo
hiện tại sau khi số đã được thay đổi, nhưng phương thức hậu tố trả về một đối tượng mới với bộ đếm trước đó được lưu, điều này phù hợp với nguyên tắc của phép tăng hậu tố.
Nếu cần, lập trình viên, tất nhiên, có thể nạp chồng bất kỳ phép toán nào theo cách tùy ý. Ví dụ, có thể tính tích, xuất số ra nhật ký, hoặc làm điều gì khác trong triển khai của phép tăng. Tuy nhiên, nên tuân theo cách tiếp cận mà việc nạp chồng toán tử thực hiện các hành động trực quan.
Chúng ta triển khai các phép toán giảm tương tự: chúng sẽ trả về số trước đó trong dãy.
Fibo *operator--() // tiền tố
{
int diff = current - previous;
current = previous;
previous = diff;
return &this;
}
Fibo operator--(int) // hậu tố
{
Fibo temp = this;
--this;
return temp;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Để lấy một số từ dãy theo số thứ tự cho trước, chúng ta sẽ nạp chồng phép toán truy cập chỉ số.
Fibo *operator[](int index)
{
current = 1;
previous = 0;
for(int i = 0; i < index; ++i)
{
++this;
}
return &this;
}
2
3
4
5
6
7
8
9
10
Để lấy số hiện tại chứa trong biến current
, hãy nạp chồng toán tử ~
(vì nó hiếm khi được sử dụng).
int operator~() const
{
return current;
}
2
3
4
Nếu không có nạp chồng này, bạn vẫn cần triển khai một phương thức công khai nào đó để đọc trường riêng current
. Chúng ta sẽ sử dụng toán tử này để xuất số với Print
.
Bạn cũng nên nạp chồng phép gán để thuận tiện.
Fibo *operator=(const Fibo &other)
{
current = other.current;
previous = other.previous;
return &this;
}
Fibo *operator=(const Fibo *other)
{
current = other.current;
previous = other.previous;
return &this;
}
2
3
4
5
6
7
8
9
10
11
12
13
Hãy kiểm tra xem tất cả hoạt động như thế nào.
void OnStart()
{
Fibo f1, f2, f3, f4;
for(int i = 0; i < 10; ++i, ++f1) // tăng tiền tố
{
f4 = f3++; // tăng hậu tố và nạp chồng phép gán
}
// so sánh tất cả các giá trị thu được bằng phép tăng và bằng chỉ số [10]
Print(~f1, " ", ~f2[10], " ", ~f3, " ", ~f4);
}
2
3
4
5
6
7
8
9
10
11
Ở đây, chúng ta đã tạo 2 ma trận với kích thước lần lượt là 3x2 và 2x3, sau đó điền giá trị từ các mảng và chỉnh sửa phần tử chọn lọc bằng cú pháp hai chỉ số [][]
. Cuối cùng, chúng ta tính biểu thức m * n + p
, trong đó tất cả các toán hạng đều là ma trận. Dòng bên dưới hiển thị cùng biểu thức dưới dạng lời gọi phương thức. Chúng ta nhận được kết quả giống nhau.
INFO
Không giống như C++, MQL5 không hỗ trợ nạp chồng toán tử ở cấp độ toàn cục. Trong MQL5, một toán tử chỉ có thể được nạp chồng trong ngữ cảnh của một lớp hoặc cấu trúc, tức là sử dụng phương thức của chúng. Ngoài ra, MQL5 không hỗ trợ nạp chồng ép kiểu, toán tử new
và delete
.