Ghi và đọc mảng
Hai hàm MQL5 được thiết kế để ghi và đọc mảng là FileWriteArray
và FileReadArray
. Với các tệp nhị phân, chúng cho phép xử lý mảng của bất kỳ loại tích hợp nào ngoại trừ chuỗi, cũng như các mảng của cấu trúc đơn giản không chứa các trường chuỗi, đối tượng, con trỏ và mảng động. Những hạn chế này liên quan đến việc tối ưu hóa quá trình ghi và đọc, điều này có thể thực hiện được nhờ loại bỏ các loại có độ dài thay đổi. Chuỗi, đối tượng và mảng động chính là như vậy.
Đồng thời, khi làm việc với tệp văn bản, các hàm này có khả năng hoạt động trên mảng kiểu string
(các loại mảng khác trong tệp có chế độ FILE_TXT
/FILE_CSV
không được các hàm này cho phép). Những mảng như vậy được lưu trữ trong tệp theo định dạng sau: một phần tử trên mỗi dòng.
Nếu bạn cần lưu trữ cấu trúc hoặc lớp mà không có hạn chế về loại trong tệp, hãy sử dụng các hàm dành riêng cho từng loại xử lý một giá trị cho mỗi lần gọi. Chúng được mô tả trong hai phần về ghi và đọc biến của các loại tích hợp: cho tệp nhị phân và văn bản.
Ngoài ra, hỗ trợ cho các cấu trúc có chuỗi có thể được tổ chức thông qua tối ưu hóa nội bộ của việc lưu trữ thông tin. Ví dụ, thay vì các trường chuỗi, bạn có thể sử dụng các trường số nguyên, chứa chỉ số của các chuỗi tương ứng trong một mảng riêng với chuỗi. Với khả năng định nghĩa lại nhiều thao tác (đặc biệt là phép gán) bằng các công cụ OOP và lấy một phần tử cấu trúc của mảng theo số, giao diện của thuật toán hầu như không thay đổi. Nhưng khi ghi, bạn có thể mở tệp ở chế độ nhị phân trước và gọi FileWriteArray
cho mảng với loại cấu trúc đơn giản hóa, sau đó mở lại tệp ở chế độ văn bản và thêm mảng của tất cả chuỗi vào đó bằng lần gọi thứ hai của FileWriteArray
. Để đọc tệp như vậy, bạn nên cung cấp một tiêu đề ở đầu tệp chứa số lượng phần tử trong các mảng để truyền nó dưới dạng tham số count
vào FileReadArray
(xem thêm ở phần sau).
Nếu bạn cần lưu hoặc đọc không phải mảng của cấu trúc mà là một cấu trúc đơn lẻ, hãy sử dụng các hàm FileWriteStruct
và FileReadStruct
, được mô tả trong phần tiếp theo.
Hãy nghiên cứu chữ ký của các hàm và sau đó xem xét một ví dụ tổng quát (FileArray.mq5
).
uint FileWriteArray(int handle, const void &array[], int start = 0, int count = WHOLE_ARRAY)
Hàm này ghi mảng array
vào tệp với mô tả handle
. Mảng có thể là đa chiều. Các tham số start
và count
cho phép đặt phạm vi của các phần tử; mặc định là toàn bộ mảng. Trong trường hợp mảng đa chiều, chỉ số start
và số lượng phần tử count
đề cập đến việc đánh số liên tục qua tất cả các chiều, không phải chiều đầu tiên của mảng. Ví dụ, nếu mảng có cấu hình [][5], thì giá trị start
bằng 7 sẽ trỏ đến phần tử có chỉ số [1][2], và count = 2
sẽ thêm phần tử [1][3] vào đó.
Hàm trả về số lượng phần tử được ghi. Trong trường hợp lỗi, nó sẽ là 0.
Nếu handle
được nhận ở chế độ nhị phân, mảng có thể thuộc bất kỳ loại tích hợp nào ngoại trừ chuỗi, hoặc các loại cấu trúc đơn giản. Nếu handle
được mở ở bất kỳ chế độ văn bản nào, mảng phải thuộc loại string
.
uint FileReadArray(int handle, const void &array[], int start = 0, int count = WHOLE_ARRAY)
Hàm này đọc dữ liệu từ tệp với mô tả handle
vào một mảng. Mảng có thể là đa chiều và động. Đối với mảng đa chiều, các tham số start
và count
hoạt động dựa trên việc đánh số liên tục các phần tử trong tất cả các chiều, như đã mô tả ở trên. Một mảng động, nếu cần, sẽ tự động tăng kích thước để phù hợp với dữ liệu được đọc. Nếu start
lớn hơn độ dài ban đầu của mảng, các phần tử trung gian này sẽ chứa dữ liệu ngẫu nhiên sau khi phân bổ bộ nhớ (xem ví dụ).
Lưu ý rằng hàm không thể kiểm soát liệu cấu hình của mảng được sử dụng khi ghi tệp có khớp với cấu hình của mảng nhận khi đọc hay không. Về cơ bản, không có đảm bảo rằng tệp được đọc đã được ghi bằng
FileWriteArray
.Để kiểm tra tính hợp lệ của cấu trúc dữ liệu, thường sử dụng một số định dạng tiêu đề ban đầu hoặc các mô tả khác bên trong tệp. Bản thân các hàm sẽ đọc bất kỳ nội dung nào của tệp trong phạm vi kích thước của nó và đặt nó vào mảng được chỉ định.
Nếu handle
được nhận ở chế độ nhị phân, mảng có thể là bất kỳ loại tích hợp không phải chuỗi nào hoặc loại cấu trúc đơn giản. Nếu handle
được mở ở chế độ văn bản, mảng phải thuộc loại string
.
Hãy kiểm tra hoạt động của cả hai ở chế độ nhị phân và văn bản bằng script FileArray.mq5
. Để làm điều này, chúng ta sẽ đặt trước hai tên tệp.
const string raw = "MQL5Book/array.raw";
const string txt = "MQL5Book/array.txt";
2
Ba mảng kiểu long
và hai mảng kiểu string
được mô tả trong hàm OnStart
. Chỉ mảng đầu tiên của mỗi loại được điền dữ liệu, và tất cả các mảng còn lại sẽ được kiểm tra để đọc sau khi tệp được ghi.
void OnStart()
{
long numbers1[][2] = {{1, 4}, {2, 5}, {3, 6}};
long numbers2[][2];
long numbers3[][2];
string text1[][2] = {{"1.0", "abc"}, {"2.0", "def"}, {"3.0", "ghi"}};
string text2[][2];
...
}
2
3
4
5
6
7
8
9
10
Ngoài ra, để kiểm tra các thao tác với cấu trúc, 3 loại sau được định nghĩa:
struct TT
{
string s1;
string s2;
};
struct B
{
private:
int b;
public:
void setB(const int v) { b = v; }
};
struct XYZ : public B
{
color x, y, z;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Chúng ta sẽ không thể sử dụng cấu trúc kiểu TT
trong các hàm được mô tả vì nó chứa các trường chuỗi. Nó cần thiết để thể hiện một lỗi biên dịch tiềm năng trong một câu lệnh được bình luận (xem thêm ở phần sau). Việc kế thừa giữa các cấu trúc B
và XYZ
, cũng như sự hiện diện của một trường đóng, không phải là trở ngại cho các hàm FileWriteArray
và FileReadArray
.
Các cấu trúc được sử dụng để khai báo một cặp mảng:
TT tt[]; // trống, vì dữ liệu không quan trọng
XYZ xyz[1];
xyz[0].setB(-1);
xyz[0].x = xyz[0].y = xyz[0].z = clrRed;
2
3
4
Hãy bắt đầu với chế độ nhị phân. Chúng ta sẽ tạo một tệp mới hoặc mở một tệp hiện có, xóa nội dung của nó. Sau đó, trong ba lần gọi FileWriteArray
, chúng ta sẽ cố gắng ghi ba mảng: numbers1
, text1
và xyz
.
int writer = PRTF(FileOpen(raw, FILE_BIN | FILE_WRITE)); // 1 / ok
PRTF(FileWriteArray(writer, numbers1)); // 6 / ok
PRTF(FileWriteArray(writer, text1)); // 0 / FILE_NOTTXT(5012)
PRTF(FileWriteArray(writer, xyz)); // 1 / ok
FileClose(writer);
ArrayPrint(numbers1);
2
3
4
5
6
Các mảng numbers1
và xyz
được ghi thành công, như được chỉ ra bởi số lượng mục được ghi. Mảng text1
thất bại với lỗi FILE_NOTTXT(5012)
vì mảng chuỗi yêu cầu tệp được mở ở chế độ văn bản. Do đó, nội dung xyz
sẽ nằm trong tệp ngay sau tất cả các phần tử của numbers1
.
Lưu ý rằng mỗi hàm ghi (hoặc đọc) bắt đầu ghi (hoặc đọc) dữ liệu vào vị trí hiện tại trong tệp và dịch chuyển nó theo kích thước của dữ liệu được ghi hoặc đọc. Nếu con trỏ này ở cuối tệp trước thao tác ghi, kích thước tệp sẽ tăng lên. Nếu cuối tệp được đạt tới trong khi đọc, con trỏ không di chuyển nữa và hệ thống đưa ra mã lỗi nội bộ đặc biệt 5027 (
FILE_ENDOFFILE
). Trong một tệp mới có kích thước bằng không, điểm đầu và cuối là như nhau.
Từ mảng text1
, 0 mục được ghi, vì vậy không có gì trong tệp nhắc nhở bạn rằng giữa hai lần gọi FileWriteArray
thành công có một lần thất bại.
Trong script thử nghiệm, chúng ta chỉ đơn giản xuất kết quả của hàm và trạng thái (mã lỗi) ra nhật ký, nhưng trong một chương trình thực tế, chúng ta nên phân tích các vấn đề ngay lập tức và thực hiện một số hành động: sửa một thứ gì đó trong tham số, trong cài đặt tệp, hoặc gián đoạn quy trình với một thông báo cho người dùng.
Hãy đọc tệp vào mảng numbers2
.
int reader = PRTF(FileOpen(raw, FILE_BIN | FILE_READ)); // 1 / ok
PRTF(FileReadArray(reader, numbers2)); // 8 / ok
ArrayPrint(numbers2);
2
3
Vì hai mảng khác nhau đã được ghi vào tệp (không chỉ numbers1
, mà còn xyz
), 8 phần tử đã được đọc vào mảng nhận (tức là toàn bộ tệp đến cuối, vì không được chỉ định khác bằng các tham số).
Thực tế, kích thước của cấu trúc XYZ
là 16 byte (4 trường 4 byte: một int
và ba color
), tương ứng với một hàng trong mảng numbers2
(2 phần tử kiểu long
). Trong trường hợp này, đó là một sự trùng hợp. Như đã lưu ý ở trên, các hàm không biết về cấu hình và kích thước của dữ liệu thô và có thể đọc bất cứ thứ gì vào bất kỳ mảng nào: lập trình viên phải theo dõi tính hợp lệ của thao tác.
Hãy so sánh trạng thái ban đầu và trạng thái nhận được. Mảng nguồn numbers1
:
[,0][,1]
[0,] 1 4
[1,] 2 5
[2,] 3 6
2
3
4
Mảng kết quả numbers2
:
[,0] [,1]
[0,] 1 4
[1,] 2 5
[2,] 3 6
[3,] 1099511627775 1095216660735
2
3
4
5
Phần đầu của mảng numbers2
hoàn toàn khớp với mảng ban đầu numbers1
, tức là việc ghi và đọc qua tệp hoạt động chính xác.
Hàng cuối cùng hoàn toàn bị chiếm bởi một cấu trúc XYZ
duy nhất (với các giá trị đúng, nhưng biểu diễn sai dưới dạng hai số kiểu long
).
Bây giờ chúng ta quay lại đầu tệp (sử dụng hàm FileSeek
, mà chúng ta sẽ thảo luận sau trong phần Kiểm soát vị trí trong tệp) và gọi FileReadArray
với số lượng và số phần tử được chỉ định, tức là thực hiện đọc một phần.
PRTF(FileSeek(reader, 0, SEEK_SET)); // true
PRTF(FileReadArray(reader, numbers3, 10, 3));
FileClose(reader);
ArrayPrint(numbers3);
2
3
4
Ba phần tử được đọc từ tệp và đặt, bắt đầu từ chỉ số 10, vào mảng nhận numbers3
. Vì tệp được đọc từ đầu, các phần tử này là giá trị 1, 4, 2. Và vì mảng hai chiều có cấu hình [][2], chỉ số xuyên suốt 10 trỏ đến phần tử [5,0]. Đây là cách nó trông trong bộ nhớ:
[,0][,1]
[0,] 1 4
[1,] 1 4
[2,] 2 6
[3,] 0 0
[4,] 0 0
[5,] 1 4
[6,] 2 0
2
3
4
5
6
7
8
Các mục được đánh dấu màu vàng là ngẫu nhiên (có thể thay đổi cho các lần chạy script khác nhau). Có thể tất cả chúng sẽ là số không, nhưng điều này không được đảm bảo. Mảng numbers3
ban đầu trống và lần gọi FileReadArray
đã khởi tạo việc phân bổ bộ nhớ cần thiết để nhận 3 phần tử tại vị trí偏移 10 (tổng cộng 13). Khối được chọn không được điền gì cả, và chỉ 3 số được đọc từ tệp. Do đó, các phần tử với chỉ số xuyên suốt từ 0 đến 9 (tức là 5 hàng đầu tiên), cũng như phần tử cuối cùng, với chỉ số 13, chứa rác.
Mảng đa chiều được mở rộng dọc theo chiều đầu tiên, và do đó việc tăng 1 số có nghĩa là thêm toàn bộ cấu hình dọc theo các chiều cao hơn. Trong trường hợp này, việc phân phối liên quan đến một chuỗi hai số ([][2]). Nói cách khác, kích thước yêu cầu 13 được làm tròn lên thành bội số của hai, tức là 14.
Cuối cùng, hãy kiểm tra cách các hàm hoạt động với mảng chuỗi. Chúng ta sẽ tạo một tệp mới hoặc mở một tệp hiện có, xóa nội dung của nó. Sau đó, trong hai lần gọi FileWriteArray
, chúng ta sẽ ghi mảng text1
và numbers1
.
writer = PRTF(FileOpen(txt, FILE_TXT | FILE_ANSI | FILE_WRITE)); // 1 / ok
PRTF(FileWriteArray(writer, text1)); // 6 / ok
PRTF(FileWriteArray(writer, numbers1)); // 0 / FILE_NOTBIN(5011)
FileClose(writer);
2
3
4
Mảng chuỗi được lưu thành công. Mảng số bị bỏ qua với lỗi FILE_NOTBIN(5011)
vì nó phải mở tệp ở chế độ nhị phân.
Khi cố gắng ghi mảng cấu trúc tt
, chúng ta nhận được lỗi biên dịch với thông báo dài "các cấu trúc hoặc lớp chứa đối tượng không được phép". Điều mà trình biên dịch thực sự muốn nói là nó không thích các trường như string
(giả định rằng chuỗi và mảng động có biểu diễn nội bộ của một số đối tượng dịch vụ). Do đó, mặc dù tệp được mở ở chế độ văn bản và chỉ có các trường văn bản trong cấu trúc, sự kết hợp này không được hỗ trợ trong MQL5.
// COMPILATION ERROR: structures or classes containing objects are not allowed
FileWriteArray(writer, tt);
2
Sự hiện diện của các trường chuỗi khiến cấu trúc trở nên "phức tạp" và không phù hợp để làm việc với các hàm FileWriteArray/FileReadArray
ở bất kỳ chế độ nào.
Sau khi chạy script, bạn có thể chuyển đến thư mục MQL5/Files/MQL5Book
và kiểm tra nội dung của các tệp được tạo.
Trước đó, trong phần Ghi và đọc tệp ở chế độ đơn giản hóa, chúng ta đã thảo luận về các hàm FileSave
và FileLoad
. Trong script thử nghiệm (FileSaveLoad.mq5
), chúng ta đã triển khai các phiên bản tương đương của các hàm này bằng cách sử dụng FileWriteArray
và FileReadArray
. Nhưng chúng ta chưa xem xét chi tiết về chúng. Vì giờ đây chúng ta đã quen với các hàm mới này, chúng ta có thể xem xét mã nguồn:
template<typename T>
bool MyFileSave(const string name, const T &array[], const int flags = 0)
{
const int h = FileOpen(name, FILE_BIN | FILE_WRITE | flags);
if(h == INVALID_HANDLE) return false;
FileWriteArray(h, array);
FileClose(h);
return true;
}
template<typename T>
long MyFileLoad(const string name, T &array[], const int flags = 0)
{
const int h = FileOpen(name, FILE_BIN | FILE_READ | flags);
if(h == INVALID_HANDLE) return -1;
const uint n = FileReadArray(h, array, 0, (int)(FileSize(h) / sizeof(T)));
// phiên bản này có thêm kiểm tra sau so với FileLoad tiêu chuẩn:
// nếu kích thước tệp không phải là bội số của kích thước cấu trúc, in cảnh báo
const ulong leftover = FileSize(h) - FileTell(h);
if(leftover != 0)
{
PrintFormat("Warning from %s: Some data left unread: %d bytes",
__FUNCTION__, leftover);
SetUserError((ushort)leftover);
}
FileClose(h);
return n;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
MyFileSave
được xây dựng dựa trên một lần gọi duy nhất của FileWriteArray
, và MyFileLoad
dựa trên lần gọi FileReadArray
, giữa một cặp gọi FileOpen/FileClose
. Trong cả hai trường hợp, tất cả dữ liệu có sẵn được ghi và đọc. Nhờ mẫu, các hàm của chúng ta cũng có khả năng chấp nhận mảng của các loại tùy ý. Nhưng nếu bất kỳ loại không được hỗ trợ nào (ví dụ, một lớp) được suy ra làm tham số meta T, thì sẽ xảy ra lỗi biên dịch, như trường hợp với việc truy cập không đúng vào các hàm tích hợp.