Ghi và đọc tệp trong chế độ đơn giản
Trong số các hàm tệp của MQL5 được thiết kế để ghi và đọc dữ liệu, có sự phân chia thành 2 nhóm không đồng đều. Nhóm đầu tiên bao gồm hai hàm: FileSave
và FileLoad
, cho phép ghi hoặc đọc dữ liệu ở chế độ nhị phân chỉ trong một lần gọi hàm. Một mặt, cách tiếp cận này có ưu điểm không thể phủ nhận là sự đơn giản, nhưng mặt khác, nó cũng có một số hạn chế (sẽ được đề cập chi tiết dưới đây). Nhóm lớn thứ hai bao gồm tất cả các hàm tệp được sử dụng khác nhau: cần gọi tuần tự nhiều hàm để thực hiện một thao tác đọc hoặc ghi hoàn chỉnh về mặt logic. Điều này có vẻ phức tạp hơn, nhưng nó mang lại sự linh hoạt và khả năng kiểm soát quá trình. Các hàm trong nhóm thứ hai hoạt động với các số nguyên đặc biệt — mô tả tệp, được lấy bằng hàm FileOpen
(xem phần tiếp theo).
Hãy xem mô tả chính thức của hai hàm này, sau đó xem xét ví dụ của chúng (FileSaveLoad.mq5
).
bool FileSave(const string filename, const void &data[], const int flag = 0)
Hàm này ghi tất cả các phần tử của mảng data
được truyền vào một tệp nhị phân có tên là filename
. Tham số filename
có thể chứa không chỉ tên tệp mà còn cả tên của các thư mục với nhiều cấp lồng nhau: hàm sẽ tạo các thư mục được chỉ định nếu chúng chưa tồn tại. Nếu tệp đã tồn tại, nó sẽ bị ghi đè (trừ khi đang bị chương trình khác占用).
Tham số data
có thể là một mảng của bất kỳ kiểu tích hợp nào, ngoại trừ chuỗi. Nó cũng có thể là một mảng của các cấu trúc đơn giản chứa các trường của kiểu tích hợp, ngoại trừ chuỗi, mảng động và con trỏ. Các lớp cũng không được hỗ trợ.
Tham số flag
, nếu cần, có thể chứa hằng số định sẵn FILE_COMMON
, nghĩa là tạo và ghi tệp vào thư mục dữ liệu chung của tất cả các terminal (Common/Files/
). Nếu không chỉ định cờ (tương ứng với giá trị mặc định là 0), thì tệp được ghi vào thư mục dữ liệu thông thường (nếu chương trình MQL đang chạy trong terminal) hoặc vào thư mục agent thử nghiệm (nếu đang chạy trong tester). Trong hai trường hợp cuối, hộp cát MQL5/Files/
được sử dụng bên trong thư mục, như đã mô tả ở đầu chương.
Hàm trả về chỉ báo thành công của thao tác (true
) hoặc lỗi (false
).
long FileLoad(const string filename, void &data[], const int flag = 0)
Hàm này đọc toàn bộ nội dung của tệp nhị phân filename
vào mảng data
được chỉ định. Tên tệp có thể bao gồm hệ thống phân cấp thư mục trong hộp cát MQL5/Files
hoặc Common/Files
.
Mảng data
phải thuộc bất kỳ kiểu tích hợp nào ngoại trừ string
, hoặc kiểu cấu trúc đơn giản (xem ở trên).
Tham số flag
kiểm soát việc chọn thư mục nơi tệp được tìm kiếm và mở: mặc định (với giá trị 0) là hộp cát tiêu chuẩn, nhưng nếu đặt giá trị FILE_COMMON
, thì đó là hộp cát chung cho tất cả các terminal.
Hàm trả về số lượng phần tử đã đọc, hoặc -1 nếu có lỗi.
Lưu ý rằng dữ liệu từ tệp được đọc theo khối của một phần tử mảng. Nếu kích thước tệp không phải là bội số của kích thước phần tử, thì dữ liệu còn lại sẽ bị bỏ qua (không được đọc). Ví dụ, nếu kích thước tệp là 10 byte, việc đọc nó vào một mảng kiểu double
(sizeof(double)=8
) sẽ chỉ tải được 8 byte, tức là 1 phần tử (và hàm sẽ trả về 1). 2 byte còn lại ở cuối tệp sẽ bị bỏ qua.
Trong script FileSaveLoad.mq5
, chúng ta định nghĩa hai cấu trúc để thử nghiệm.
struct Pair
{
short x, y;
};
struct Simple
{
double d;
int i;
datetime t;
color c;
uchar a[10]; // mảng kích thước cố định được phép
bool b;
Pair p; // các trường phức hợp (cấu trúc đơn giản lồng nhau) cũng được phép
// chuỗi và mảng động sẽ gây lỗi biên dịch khi sử dụng
// FileSave/FileLoad: cấu trúc hoặc lớp chứa đối tượng không được phép
// string s;
// uchar a[];
// con trỏ cũng không được hỗ trợ
// void *ptr;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Cấu trúc Simple
chứa các trường của hầu hết các kiểu được phép, cũng như một trường phức hợp với kiểu cấu trúc Pair
. Trong hàm OnStart
, chúng ta điền một mảng nhỏ kiểu Simple
.
void OnStart()
{
Simple write[] =
{
{+1.0, -1, D'2021.01.01', clrBlue, {'a'}, true, {1000, 16000}},
{-1.0, -2, D'2021.01.01', clrRed, {'b'}, true, {1000, 16000}},
};
...
2
3
4
5
6
7
8
Chúng ta sẽ chọn tệp để ghi dữ liệu cùng với thư mục con MQL5Book
để các thử nghiệm của chúng ta không lẫn với các tệp làm việc của bạn:
const string filename = "MQL5Book/rawdata";
Hãy ghi mảng vào tệp, đọc nó vào một mảng khác và so sánh chúng.
PRT(FileSave(filename, write/*, FILE_COMMON*/)); // true
Simple read[];
PRT(FileLoad(filename, read/*, FILE_COMMON*/)); // 2
PRT(ArrayCompare(write, read)); // 0
2
3
4
5
6
FileLoad
trả về 2, tức là 2 phần tử (2 cấu trúc) đã được đọc. Nếu kết quả so sánh là 0, điều đó có nghĩa là dữ liệu khớp nhau. Bạn có thể mở thư mục trong trình quản lý tệp yêu thích của mình MQL5/Files/MQL5Book
và đảm bảo rằng tệp rawdata
tồn tại (không nên xem nội dung của nó bằng trình chỉnh sửa văn bản, chúng ta khuyên bạn nên sử dụng trình xem hỗ trợ chế độ nhị phân).
Tiếp theo trong script, chúng ta chuyển đổi mảng cấu trúc đã đọc thành byte và xuất chúng ra nhật ký dưới dạng mã thập lục phân. Đây là một dạng bản sao bộ nhớ, giúp bạn hiểu tệp nhị phân là gì.
uchar bytes[];
for(int i = 0; i < ArraySize(read); ++i)
{
uchar temp[];
PRT(StructToCharArray(read[i], temp));
ArrayCopy(bytes, temp, ArraySize(bytes));
}
ByteArrayPrint(bytes);
2
3
4
5
6
7
8
Kết quả:
[00] 00 | 00 | 00 | 00 | 00 | 00 | F0 | 3F | FF | FF | FF | FF | 00 | 66 | EE | 5F |
[16] 00 | 00 | 00 | 00 | 00 | 00 | FF | 00 | 61 | 00 | 00 | 00 | 00 | 00 | 00 | 00 |
[32] 00 | 00 | 01 | E8 | 03 | 80 | 3E | 00 | 00 | 00 | 00 | 00 | 00 | F0 | BF | FE |
[48] FF | FF | FF | 00 | 66 | EE | 5F | 00 | 00 | 00 | 00 | FF | 00 | 00 | 00 | 62 |
[64] 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 01 | E8 | 03 | 80 | 3E |
2
3
4
5
Vì hàm tích hợp ArrayPrint
không thể in ở định dạng thập lục phân, chúng ta đã phải phát triển hàm riêng ByteArrayPrint
(ở đây chúng ta không cung cấp mã nguồn của nó, hãy xem tệp đính kèm).
Tiếp theo, hãy nhớ rằng FileLoad
có thể tải dữ liệu vào mảng của bất kỳ kiểu nào, vì vậy chúng ta sẽ đọc cùng tệp đó trực tiếp vào một mảng byte.
uchar bytes2[];
PRT(FileLoad(filename, bytes2/*, FILE_COMMON*/)); // 78, 39 * 2
PRT(ArrayCompare(bytes, bytes2)); // 0, equality
2
3
Việc so sánh thành công hai mảng byte cho thấy FileLoad
có thể hoạt động với dữ liệu thô từ tệp theo cách tùy ý mà nó được chỉ dẫn (không có thông tin trong tệp rằng nó lưu trữ mảng cấu trúc Simple
).
Điều quan trọng cần lưu ý ở đây là vì kiểu byte có kích thước tối thiểu (1), nó là bội số của bất kỳ kích thước tệp nào. Do đó, bất kỳ tệp nào cũng luôn được đọc vào mảng byte mà không có dư. Ở đây hàm FileLoad
đã trả về số 78 (số phần tử bằng số byte). Đây là kích thước của tệp (hai cấu trúc, mỗi cấu trúc 39 byte).
Về cơ bản, khả năng của FileLoad
trong việc diễn giải dữ liệu cho bất kỳ kiểu nào đòi hỏi sự cẩn thận và kiểm tra từ phía lập trình viên. Cụ thể, tiếp theo trong script, chúng ta đọc cùng tệp đó vào một mảng cấu trúc MqlDateTime
. Điều này, tất nhiên, là sai, nhưng nó hoạt động mà không có lỗi.
MqlDateTime mdt[];
PRT(sizeof(MqlDateTime)); // 32
PRT(FileLoad(filename, mdt)); // 2
// chú ý: 14 byte còn lại không được đọc
ArrayPrint(mdt);
2
3
4
5
Kết quả chứa một tập hợp số vô nghĩa:
[year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0] 0 1072693248 -1 1609459200 0 16711680 97 0
[1] -402587648 4096003 0 -20975616 16777215 6286950 -16777216 1644167168
2
3
Vì kích thước của MqlDateTime
là 32, nên chỉ hai cấu trúc như vậy phù hợp trong tệp 78 byte, và còn lại 14 byte dư thừa. Sự hiện diện của phần dư cho thấy có vấn đề. Nhưng ngay cả khi không có phần dư, điều này không đảm bảo tính hợp lý của thao tác được thực hiện, vì hai kích thước khác nhau có thể, hoàn toàn tình cờ, phù hợp với số lần nguyên (nhưng khác nhau) trong độ dài của tệp. Hơn nữa, hai cấu trúc khác nhau về ý nghĩa có thể có cùng kích thước, nhưng điều đó không có nghĩa là chúng nên được ghi và đọc từ cái này sang cái kia.
Không có gì ngạc nhiên khi nhật ký của mảng cấu trúc MqlDateTime
cho thấy các giá trị kỳ lạ, vì thực tế đó là một kiểu dữ liệu hoàn toàn khác.
Để việc đọc cẩn thận hơn, script triển khai một hàm tương tự FileLoad
— MyFileLoad
. Chúng ta sẽ phân tích chi tiết hàm này, cũng như cặp của nó MyFileSave
, trong các phần tiếp theo, khi tìm hiểu các hàm tệp mới và sử dụng chúng để mô phỏng cấu trúc bên trong của FileSave/FileLoad
. Trong thời gian này, chỉ cần lưu ý rằng trong phiên bản của chúng ta, chúng ta có thể kiểm tra sự hiện diện của phần dư chưa đọc trong tệp và hiển thị cảnh báo.
Để kết thúc, hãy xem xét một vài lỗi tiềm ẩn khác được thể hiện trong script.
/*
// lỗi biên dịch, kiểu chuỗi không được hỗ trợ ở đây
string texts[];
FileSave("any", texts); // chuyển đổi tham số không được phép
*/
double data[];
PRT(FileLoad("any", data)); // -1
PRT(_LastError); // 5004, ERR_CANNOT_OPEN_FILE
2
3
4
5
6
7
8
9
Lỗi đầu tiên xảy ra tại thời điểm biên dịch (đó là lý do khối mã được chú thích) vì mảng chuỗi không được phép.
Lỗi thứ hai là đọc một tệp không tồn tại, đó là lý do FileLoad
trả về -1. Mã lỗi giải thích có thể dễ dàng lấy được bằng GetLastError
(hoặc _LastError).