Làm Việc Với Ký Tự và Bảng Mã
Vì chuỗi được tạo thành từ các ký tự, đôi khi cần thiết hoặc đơn giản là tiện lợi hơn khi thao tác với từng ký tự riêng lẻ hoặc nhóm ký tự trong chuỗi ở mức mã số nguyên của chúng. Ví dụ, bạn cần đọc hoặc thay thế từng ký tự một hoặc chuyển đổi chúng thành mảng mã số nguyên để truyền qua các giao thức giao tiếp hoặc vào giao diện lập trình của bên thứ ba của các thư viện động DLL. Trong tất cả các trường hợp như vậy, việc truyền chuỗi dưới dạng văn bản có thể đi kèm với nhiều khó khăn khác nhau:
- Đảm bảo mã hóa chính xác (có rất nhiều loại mã hóa, và việc chọn một loại cụ thể phụ thuộc vào vùng cài đặt của hệ điều hành, cài đặt chương trình, cấu hình của các máy chủ mà giao tiếp được thực hiện, và nhiều yếu tố khác).
- Chuyển đổi các ký tự ngôn ngữ quốc gia từ mã hóa văn bản cục bộ sang Unicode và ngược lại.
- Phân bổ và giải phóng bộ nhớ theo cách thống nhất.
Việc sử dụng mảng với các mã số nguyên (mặc dù việc sử dụng này thực sự tạo ra một biểu diễn nhị phân thay vì văn bản của chuỗi) giúp đơn giản hóa những vấn đề này.
API MQL5 cung cấp một tập hợp các hàm để thao tác trên từng ký tự riêng lẻ hoặc nhóm ký tự của chúng, có tính đến các đặc điểm mã hóa.
Chuỗi trong MQL5 chứa các ký tự trong mã hóa Unicode hai byte. Điều này cung cấp sự hỗ trợ phổ quát cho toàn bộ sự đa dạng của các bảng chữ cái quốc gia trong một bảng ký tự duy nhất (nhưng rất lớn). Hai byte cho phép mã hóa 65535 phần tử.
Kiểu ký tự mặc định là ushort
. Tuy nhiên, nếu cần, chuỗi có thể được chuyển đổi thành một chuỗi các ký tự một byte uchar
trong một mã hóa ngôn ngữ cụ thể. Việc chuyển đổi này có thể đi kèm với việc mất một số thông tin (đặc biệt, các chữ cái không có trong bảng ký tự cục bộ có thể "mất" dấu hoặc thậm chí "biến thành" một ký tự thay thế nào đó: tùy thuộc vào ngữ cảnh, nó có thể được hiển thị khác nhau, nhưng thường là ?
hoặc một ký tự hình vuông).
WARNING
Để tránh các vấn đề với văn bản có thể chứa các ký tự bất kỳ, nên luôn sử dụng Unicode. Ngoại lệ có thể được áp dụng nếu một số dịch vụ hoặc chương trình bên ngoài cần tích hợp với chương trình MQL của bạn không hỗ trợ Unicode, hoặc nếu văn bản được thiết kế từ đầu để lưu trữ một tập hợp ký tự hạn chế (ví dụ, chỉ có số và chữ cái Latinh).
Khi chuyển đổi sang/từ các ký tự một byte, API MQL5 mặc định sử dụng mã hóa ANSI, tùy thuộc vào cài đặt Windows hiện tại. Tuy nhiên, nhà phát triển có thể chỉ định một bảng mã khác (xem các hàm tiếp theo như CharArrayToString
, StringToCharArray
).
Ví dụ về cách sử dụng các hàm được mô tả dưới đây được đưa ra trong tệp StringSymbols.mq5
.
bool StringSetCharacter(string &variable, int position, ushort character)
Hàm này thay đổi ký tự tại vị trí position
thành giá trị character
trong chuỗi variable
được truyền vào. Số vị trí phải nằm trong khoảng từ 0 đến độ dài chuỗi (StringLen
) trừ 1.
Nếu ký tự được ghi là 0, nó chỉ định một kết thúc dòng mới (hoạt động như một số 0 kết thúc), tức là độ dài của dòng trở thành bằng position
. Kích thước của bộ đệm được phân bổ cho dòng không thay đổi.
Nếu tham số position
bằng độ dài của chuỗi và ký tự được ghi không phải là 0, thì ký tự đó được thêm vào chuỗi và độ dài của nó tăng thêm 1. Điều này tương đương với biểu thức: variable += ShortToString(character)
.
Hàm trả về true
khi hoàn thành thành công, hoặc false
trong trường hợp có lỗi.
void OnStart()
{
string numbers = "0123456789";
PRT(numbers);
PRT(StringSetCharacter(numbers, 7, 0)); // cắt bỏ tại ký tự thứ 7
PRT(numbers); // 0123456
PRT(StringSetCharacter(numbers, StringLen(numbers), '*')); // thêm '*'
PRT(numbers); // 0123456*
...
}
2
3
4
5
6
7
8
9
10
ushort StringGetCharacter(string value, int position)
Hàm này trả về mã của ký tự nằm ở vị trí được chỉ định trong chuỗi. Số vị trí phải nằm trong khoảng từ 0 đến độ dài chuỗi (StringLen
) trừ 1. Trong trường hợp có lỗi, hàm sẽ trả về 0.
Hàm này tương đương với việc sử dụng toán tử []
: value[position]
.
string numbers = "0123456789";
PRT(StringGetCharacter(numbers, 5)); // 53 = mã '5'
PRT(numbers[5]); // 53 - giống nhau
2
3
string CharToString(uchar code)
Hàm này chuyển đổi mã ANSI của một ký tự thành chuỗi một ký tự. Tùy thuộc vào bảng mã Windows được thiết lập, nửa trên của các mã (lớn hơn 127) có thể tạo ra các chữ cái khác nhau (kiểu ký tự khác nhau, trong khi mã vẫn giữ nguyên). Ví dụ, ký tự với mã 0xB8 (184 trong hệ thập phân) biểu thị dấu móc dưới (cedilla) trong các ngôn ngữ Tây Âu, trong khi trong tiếng Nga, chữ cái 'ё' nằm ở đây. Dưới đây là một ví dụ khác:
PRT(CharToString(0xA9)); // "©"
PRT(CharToString(0xE6)); // "æ", "ж", hoặc ký tự khác
// tùy thuộc vào cài đặt vùng của Windows
2
3
string ShortToString(ushort code)
Hàm này chuyển đổi mã Unicode của một ký tự thành chuỗi một ký tự. Đối với tham số code
, bạn có thể sử dụng ký tự chữ hoặc số nguyên. Ví dụ, chữ cái in hoa Hy Lạp "sigma" (dấu tổng trong công thức toán học) có thể được chỉ định là 0x3A3 hoặc Σ
.
PRT(ShortToString(0x3A3)); // "Σ"
PRT(ShortToString('Σ')); // "Σ"
2
int StringToShortArray(const string text, ushort &array[], int start = 0, int count = -1)
Hàm này chuyển đổi một chuỗi thành một chuỗi mã ký tự ushort
được sao chép vào vị trí được chỉ định trong mảng: bắt đầu từ phần tử được đánh số start
(mặc định là 0, tức là đầu mảng) và với số lượng count
.
Lưu ý: tham số
start
đề cập đến vị trí trong mảng, không phải trong chuỗi. Nếu bạn muốn chuyển đổi một phần của chuỗi, trước tiên bạn phải trích xuất nó bằng hàmStringSubstr
.
Nếu tham số count
bằng -1 (hoặc WHOLE_ARRAY), tất cả các ký tự cho đến cuối chuỗi (bao gồm cả số 0 kết thúc) hoặc các ký tự theo kích thước của mảng, nếu nó có kích thước cố định, sẽ được sao chép.
Trong trường hợp mảng động, nó sẽ tự động tăng kích thước nếu cần. Nếu kích thước của mảng động lớn hơn độ dài chuỗi, thì kích thước mảng không giảm.
Để sao chép các ký tự mà không có số 0 kết thúc, bạn phải gọi rõ ràng StringLen
làm đối số count
. Nếu không, độ dài của mảng sẽ lớn hơn độ dài chuỗi 1 đơn vị (và 0 ở phần tử cuối cùng).
Hàm trả về số lượng ký tự đã sao chép.
...
ushort array1[], array2[]; // mảng động
ushort text[5]; // mảng kích thước cố định
string alphabet = "ABCDEАБВГД";
// sao chép với '0' kết thúc
PRT(StringToShortArray(alphabet, array1)); // 11
ArrayPrint(array1); // 65 66 67 68 69 1040 1041 1042 1043 1044 0
// sao chép không có '0' kết thúc
PRT(StringToShortArray(alphabet, array2, 0, StringLen(alphabet))); // 10
ArrayPrint(array2); // 65 66 67 68 69 1040 1041 1042 1043 1044
// sao chép vào mảng cố định
PRT(StringToShortArray(alphabet, text)); // 5
ArrayPrint(text); // 65 66 67 68 69
// sao chép vượt quá giới hạn trước đó của mảng
// (các phần tử [11-19] sẽ ngẫu nhiên)
PRT(StringToShortArray(alphabet, array2, 20)); // 11
ArrayPrint(array2);
/*
[ 0] 65 66 67 68 69 1040 1041 1042
1043 1044 0 0 0 0 0 14245
[16] 15102 37754 48617 54228 65 66 67 68
69 1040 1041 1042 1043 1044 0
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Lưu ý rằng nếu vị trí sao chép vượt quá kích thước của mảng, thì các phần tử trung gian sẽ được phân bổ nhưng không được khởi tạo. Kết quả là, chúng có thể chứa dữ liệu ngẫu nhiên (được đánh dấu màu vàng ở trên).
string ShortArrayToString(const ushort &array[], int start = 0, int count = -1)
Hàm này chuyển đổi một phần của mảng với mã ký tự thành chuỗi. Phạm vi các phần tử mảng được thiết lập bởi các tham số start
và count
, lần lượt là vị trí bắt đầu và số lượng. Tham số start
phải nằm trong khoảng từ 0 đến số phần tử trong mảng trừ 1. Nếu count
bằng -1 (hoặc WHOLE_ARRAY), tất cả các phần tử cho đến cuối mảng hoặc cho đến số 0 đầu tiên sẽ được sao chép.
Sử dụng cùng ví dụ từ StringSymbols.mq5
, hãy thử chuyển đổi một mảng thành chuỗi array2
, có kích thước 30.
...
string s = ShortArrayToString(array2, 0, 30);
PRT(s); // "ABCDEАБВГД", có thể xuất hiện thêm ký tự ngẫu nhiên tại đây
2
3
Vì trong mảng array2
, chuỗi "ABCDEABCD" đã được sao chép hai lần, cụ thể là lần đầu tiên vào ngay đầu mảng và lần thứ hai tại vị trí offset 20, các ký tự trung gian sẽ ngẫu nhiên và có thể tạo thành một chuỗi dài hơn dự kiến.
int StringToCharArray(const string text, uchar &array[], int start = 0, int count = -1, uint codepage = CP_ACP)
Hàm này chuyển đổi chuỗi text
thành một chuỗi ký tự một byte được sao chép vào vị trí được chỉ định trong mảng: bắt đầu từ phần tử được đánh số start
(mặc định là 0, tức là đầu mảng) và với số lượng count
. Quá trình sao chép chuyển đổi các ký tự từ Unicode sang bảng mã được chọn codepage
— mặc định là CP_ACP, nghĩa là ngôn ngữ của hệ điều hành Windows (xem thêm bên dưới).
Nếu tham số count
bằng -1 (hoặc WHOLE_ARRAY), tất cả các ký tự cho đến cuối chuỗi (bao gồm cả số 0 kết thúc) hoặc theo kích thước của mảng, nếu nó có kích thước cố định, sẽ được sao chép.
Trong trường hợp mảng động, nó sẽ tự động tăng kích thước nếu cần. Nếu kích thước của mảng động lớn hơn độ dài chuỗi, thì kích thước mảng không giảm.
Để sao chép các ký tự mà không có số 0 kết thúc, bạn phải gọi rõ ràng StringLen
làm đối số count
.
Hàm trả về số lượng ký tự đã sao chép.
Xem danh sách các bảng mã hợp lệ cho tham số codepage
trong tài liệu. Dưới đây là một số bảng mã ANSI được sử dụng rộng rãi:
Ngôn ngữ | Mã |
---|---|
Trung Âu Latinh | 1250 |
Kirin | 1251 |
Tây Âu Latinh | 1252 |
Hy Lạp | 1253 |
Thổ Nhĩ Kỳ | 1254 |
Do Thái | 1255 |
Ả Rập | 1256 |
Baltic | 1257 |
Do đó, trên các máy tính với ngôn ngữ Tây Âu, CP_ACP là 1252, và ví dụ, trên các máy tính với tiếng Nga, nó là 1251.
Trong quá trình chuyển đổi, một số ký tự có thể bị chuyển đổi với mất mát thông tin, vì bảng Unicode lớn hơn nhiều so với ANSI (mỗi bảng mã ANSI có 256 ký tự).
Liên quan đến điều này, CP_UTF8 có tầm quan trọng đặc biệt trong số tất cả các hằng số CP_***. Nó cho phép bảo toàn các ký tự quốc gia một cách chính xác bằng mã hóa độ dài biến thiên: mảng kết quả vẫn lưu trữ các byte, nhưng mỗi ký tự quốc gia có thể kéo dài qua nhiều byte, được viết ở định dạng đặc biệt. Do đó, độ dài của mảng có thể lớn hơn đáng kể so với độ dài chuỗi. Mã hóa UTF-8 được sử dụng rộng rãi trên Internet và trong nhiều phần mềm khác nhau. Nhân tiện, UTF là viết tắt của Unicode Transformation Format, và còn có các biến thể khác, đáng chú ý là UTF-16 và UTF-32.
Chúng ta sẽ xem xét ví dụ cho StringToCharArray
sau khi làm quen với hàm "ngược" CharArrayToString
: hoạt động của chúng phải được thể hiện cùng nhau.
string CharArrayToString(const uchar &array[], int start = 0, int count = -1, uint codepage = CP_ACP)
Hàm này chuyển đổi một mảng byte hoặc một phần của nó thành chuỗi. Mảng phải chứa các ký tự trong một mã hóa cụ thể. Phạm vi các phần tử mảng được thiết lập bởi các tham số start
và count
, lần lượt là vị trí bắt đầu và số lượng. Tham số start
phải nằm trong khoảng từ 0 đến số phần tử trong mảng. Khi count
bằng -1 (hoặc WHOLE_ARRAY), tất cả các phần tử cho đến cuối mảng hoặc cho đến số 0 đầu tiên sẽ được sao chép.
Hãy xem cách các hàm StringToCharArray
và CharArrayToString
hoạt động với các ký tự quốc gia khác nhau với các cài đặt bảng mã khác nhau. Một kịch bản thử nghiệm StringCodepages.mq5
đã được chuẩn bị cho việc này.
Hai dòng sẽ được sử dụng làm đối tượng thử nghiệm - bằng tiếng Nga và tiếng Đức:
void OnStart()
{
Print("Locales");
uchar bytes1[], bytes2[];
string german = "straßenführung";
string russian = "Русский текст";
...
}
2
3
4
5
6
7
8
9
Chúng ta sẽ sao chép chúng vào các mảng bytes1
và bytes2
rồi khôi phục chúng thành chuỗi.
Đầu tiên, hãy chuyển đổi văn bản tiếng Đức bằng bảng mã châu Âu 1252.
...
StringToCharArray(german, bytes1, 0, WHOLE_ARRAY, 1252);
ArrayPrint(bytes1);
// 115 116 114 97 223 101 110 102 252 104 114 117 110 103 0
2
3
4
Trên các bản sao Windows châu Âu, điều này tương đương với một lời gọi hàm đơn giản hơn với các tham số mặc định, vì ở đó CP_ACP = 1252:
StringToCharArray(german, bytes1);
Sau đó, chúng ta khôi phục văn bản từ mảng với lời gọi sau và đảm bảo rằng mọi thứ khớp với bản gốc:
...
PRT(CharArrayToString(bytes1, 0, WHOLE_ARRAY, 1252));
// CharArrayToString(bytes1,0,WHOLE_ARRAY,1252)='straßenführung'
2
3
Bây giờ hãy thử chuyển đổi văn bản tiếng Nga trong cùng mã hóa châu Âu (hoặc bạn có thể gọi StringToCharArray(russian, bytes2)
trong môi trường Windows nơi CP_ACP được đặt thành 1252 làm bảng mã mặc định):
...
StringToCharArray(russian, bytes2, 0, WHOLE_ARRAY, 1252);
ArrayPrint(bytes2);
// 63 63 63 63 63 63 63 32 63 63 63 63 63 0
2
3
4
Ở đây bạn đã có thể thấy rằng có vấn đề trong quá trình chuyển đổi vì 1252 không có chữ Kirin. Việc khôi phục chuỗi từ mảng thể hiện rõ bản chất:
...
PRT(CharArrayToString(bytes2, 0, WHOLE_ARRAY, 1252));
// CharArrayToString(bytes2,0,WHOLE_ARRAY,1252)='??????? ?????'
2
3
Hãy lặp lại thí nghiệm trong một môi trường tiếng Nga giả định, tức là chúng ta sẽ chuyển đổi cả hai chuỗi qua lại bằng bảng mã Kirin 1251.
...
StringToCharArray(russian, bytes2, 0, WHOLE_ARRAY, 1251);
// trên Windows tiếng Nga, lời gọi này tương đương với một lời gọi đơn giản hơn
// StringToCharArray(russian, bytes2);
// vì CP_ACP = 1251
ArrayPrint(bytes2); // lần này mã ký tự có ý nghĩa
// 208 243 241 241 234 232 233 32 210 229 234 241 242 0
// khôi phục chuỗi và đảm bảo nó khớp với bản gốc
PRT(CharArrayToString(bytes2, 0, WHOLE_ARRAY, 1251));
// CharArrayToString(bytes2,0,WHOLE_ARRAY,1251)='Русский Текст'
// và đối với văn bản tiếng Đức...
StringToCharArray(german, bytes1, 0, WHOLE_ARRAY, 1251);
ArrayPrint(bytes1);
// 115 116 114 97 63 101 110 102 117 104 114 117 110 103 0
// nếu chúng ta so sánh nội dung này của bytes1 với phiên bản trước,
// dễ thấy rằng một vài ký tự bị ảnh hưởng; đây là điều đã xảy ra:
// 115 116 114 97 223 101 110 102 252 104 114 117 110 103 0
// khôi phục chuỗi để thấy sự khác biệt trực quan:
PRT(CharArrayToString(bytes1, 0, WHOLE_ARRAY, 1251));
// CharArrayToString(bytes1,0,WHOLE_ARRAY,1251)='stra?enfuhrung'
// các ký tự đặc trưng của tiếng Đức đã bị hỏng
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Do đó, sự mong manh của các mã hóa một byte là rõ ràng.
Cuối cùng, hãy kích hoạt mã hóa CP_UTF8 cho cả hai chuỗi thử nghiệm. Phần này của ví dụ sẽ hoạt động ổn định bất kể cài đặt Windows.
...
StringToCharArray(german, bytes1, 0, WHOLE_ARRAY, CP_UTF8);
ArrayPrint(bytes1);
// 115 116 114 97 195 159 101 110 102 195 188 104 114 117 110 103 0
PRT(CharArrayToString(bytes1, 0, WHOLE_ARRAY, CP_UTF8));
// CharArrayToString(bytes1,0,WHOLE_ARRAY,CP_UTF8)='straßenführung'
StringToCharArray(russian, bytes2, 0, WHOLE_ARRAY, CP_UTF8);
ArrayPrint(bytes2);
// 208 160 209 131 209 129 209 129 208 186 208 184 208 185
// 32 208 162 208 181 208 186 209 129 209 130 0
PRT(CharArrayToString(bytes2, 0, WHOLE_ARRAY, CP_UTF8));
// CharArrayToString(bytes2,0,WHOLE_ARRAY,CP_UTF8)='Русский Текст'
2
3
4
5
6
7
8
9
10
11
12
13
Lưu ý rằng cả hai chuỗi được mã hóa UTF-8 đều yêu cầu mảng lớn hơn so với ANSI. Hơn nữa, mảng với văn bản tiếng Nga thực sự đã dài gấp đôi, vì tất cả các chữ cái giờ đây chiếm 2 byte. Những ai muốn có thể tìm hiểu chi tiết trong các nguồn mở về cách hoạt động của mã hóa UTF-8. Trong bối cảnh của cuốn sách này, điều quan trọng đối với chúng ta là API MQL5 cung cấp các hàm sẵn sàng để làm việc với nó.