Ghi và đọc biến (tệp nhị phân)
Nếu một cấu trúc chứa các trường thuộc các kiểu bị cấm đối với cấu trúc đơn giản (chuỗi, mảng động, con trỏ), thì sẽ không thể ghi nó vào tệp hoặc đọc từ tệp bằng các hàm đã xem xét trước đó. Điều này cũng áp dụng cho các đối tượng lớp. Tuy nhiên, các thực thể như vậy thường chứa phần lớn dữ liệu trong chương trình và cũng cần lưu và khôi phục trạng thái của chúng.
Sử dụng ví dụ về cấu trúc tiêu đề trong phần trước, đã rõ ràng rằng chuỗi (và các kiểu dữ liệu có độ dài thay đổi khác) có thể được tránh, nhưng trong trường hợp này, người ta phải nghĩ ra các triển khai thuật toán thay thế, phức tạp hơn (ví dụ, thay thế chuỗi bằng mảng ký tự).
Để ghi và đọc dữ liệu có độ phức tạp bất kỳ, MQL5 cung cấp các tập hợp hàm cấp thấp hơn hoạt động trên một giá trị đơn lẻ của một kiểu cụ thể: double
, float
, int/uint
, long/ulong
, hoặc string
. Tất cả các kiểu tích hợp khác của MQL5 tương đương với các số nguyên có kích thước khác nhau: char/uchar
là 1 byte, short/ushort
là 2 byte, color
là 4 byte, các liệt kê là 4 byte, và datetime
là 8 byte. Các hàm như vậy có thể được gọi là nguyên tử (tức là không thể chia nhỏ), vì các hàm để đọc và ghi vào tệp ở cấp độ bit không còn tồn tại.
Tất nhiên, việc ghi hoặc đọc từng phần tử cũng loại bỏ hạn chế đối với các thao tác tệp với mảng động.
Đối với các con trỏ tới đối tượng, theo tinh thần của mô hình lập trình hướng đối tượng (OOP), chúng ta có thể cho phép chúng lưu và khôi phục các đối tượng: chỉ cần triển khai trong mỗi lớp một giao diện (tập hợp các phương thức) chịu trách nhiệm chuyển nội dung quan trọng sang tệp và ngược lại, sử dụng các hàm cấp thấp. Sau đó, nếu chúng ta gặp một trường con trỏ tới một đối tượng khác trong đối tượng, chúng ta chỉ cần giao phó việc lưu hoặc đọc cho nó, và đến lượt nó, nó sẽ xử lý các trường của mình, trong đó có thể có các con trỏ khác, và việc giao phó sẽ tiếp tục sâu hơn cho đến khi bao phủ tất cả các phần tử.
Lưu ý rằng trong phần này, chúng ta sẽ xem xét các hàm nguyên tử cho tệp nhị phân. Các hàm tương đương cho tệp văn bản sẽ được trình bày trong phần tiếp theo. Tất cả các hàm trong phần này trả về số byte đã ghi, hoặc 0 trong trường hợp lỗi.
uint FileWriteDouble(int handle, double value)
uint FileWriteFloat(int handle, float value)
uint FileWriteLong(int handle, long value)
Các hàm này ghi giá trị của kiểu tương ứng được truyền trong tham số value
(double
, float
, long
) vào tệp nhị phân với mô tả handle
.
uint FileWriteInteger(int handle, int value, int size = INT_VALUE)
Hàm này ghi số nguyên value
vào tệp nhị phân với mô tả handle
. Kích thước của giá trị tính bằng byte được đặt bởi tham số size
và có thể là một trong các hằng số định sẵn: CHAR_VALUE
(1), SHORT_VALUE
(2), INT_VALUE
(4, mặc định), tương ứng với các kiểu char
, short
và int
(có dấu và không dấu).
Hàm hỗ trợ một chế độ ghi số nguyên 3 byte không được ghi nhận trong tài liệu. Việc sử dụng nó không được khuyến nghị.
Con trỏ tệp di chuyển theo số byte đã ghi (không phải theo kích thước của int
).
uint FileWriteString(int handle, const string value, int length = -1)
Hàm này ghi một chuỗi từ tham số value
vào tệp nhị phân với mô tả handle
. Bạn có thể chỉ định số ký tự sẽ ghi bằng tham số length
. Nếu nó nhỏ hơn độ dài của chuỗi, chỉ phần được chỉ định của chuỗi sẽ được đưa vào tệp. Nếu length
là -1 hoặc không được chỉ định, toàn bộ chuỗi sẽ được chuyển vào tệp mà không có ký tự null kết thúc. Nếu length
lớn hơn độ dài của chuỗi, các ký tự thừa sẽ được điền bằng số 0.
Lưu ý rằng khi ghi vào tệp được mở với cờ FILE_UNICODE
(hoặc không có cờ FILE_ANSI
), chuỗi được lưu ở định dạng Unicode (mỗi ký tự chiếm 2 byte). Khi ghi vào tệp được mở với cờ FILE_ANSI
, mỗi ký tự chiếm 1 byte (các ký tự ngoại ngữ có thể bị méo mó).
Hàm
FileWriteString
cũng có thể hoạt động với tệp văn bản. Khía cạnh này của ứng dụng của nó được mô tả trong phần tiếp theo.
double FileReadDouble(int handle)
float FileReadFloat(int handle)
long FileReadLong(int handle)
Các hàm này đọc một số thuộc kiểu phù hợp, double
, float
hoặc long
, từ tệp nhị phân với mô tả được chỉ định. Nếu cần, hãy chuyển đổi kết quả sang ulong
(nếu một số nguyên dài không dấu được mong đợi trong tệp tại vị trí đó).
int FileReadInteger(int handle, int size = INT_VALUE)
Hàm này đọc một giá trị số nguyên từ tệp nhị phân với mô tả handle
. Kích thước giá trị tính bằng byte được chỉ định trong tham số size
.
Vì kết quả của hàm thuộc kiểu int
, nó phải được chuyển đổi rõ ràng sang kiểu đích mong muốn nếu nó khác với int
(tức là sang uint
, hoặc short/ushort
, hoặc char/uchar
). Nếu không, bạn sẽ nhận được ít nhất một cảnh báo từ trình biên dịch và nhiều nhất là mất dấu.
Sự thật là khi đọc CHAR_VALUE
hoặc SHORT_VALUE
, kết quả mặc định luôn là dương (tức là tương ứng với uchar
và ushort
, vốn hoàn toàn "vừa" trong int
). Trong những trường hợp này, nếu các số thực sự thuộc kiểu uchar
và ushort
, các cảnh báo của trình biên dịch chỉ mang tính hình thức, vì chúng ta đã chắc chắn rằng bên trong giá trị kiểu int
, chỉ có 1 hoặc 2 byte thấp được điền, và chúng không có dấu. Điều này diễn ra mà không bị méo mó.
Tuy nhiên, khi lưu trữ các giá trị có dấu (kiểu char
và short
) trong tệp, việc chuyển đổi trở nên cần thiết vì nếu không, các giá trị âm sẽ biến thành các giá trị dương ngược lại với cùng biểu diễn bit (xem phần Signed and unsigned integers
trong mục Chuyển đổi kiểu số học).
Dù sao đi nữa, tốt hơn là tránh các cảnh báo bằng cách chuyển đổi kiểu rõ ràng.
Hàm hỗ trợ chế độ đọc số nguyên 3 byte. Việc sử dụng nó không được khuyến nghị.
Con trỏ tệp di chuyển theo số byte đã đọc (không phải theo kích thước int
).
string FileReadString(int handle, int size = -1)
Hàm này đọc một chuỗi có kích thước được chỉ định tính bằng ký tự từ tệp với mô tả handle
. Tham số size
phải được đặt khi làm việc với tệp nhị phân (giá trị mặc định chỉ phù hợp với tệp văn bản sử dụng ký tự phân cách). Nếu không, chuỗi không được đọc (hàm trả về chuỗi rỗng), và mã lỗi nội bộ _LastError
là 5016 (FILE_BINSTRINGSIZE
).
Do đó, ngay cả ở giai đoạn ghi chuỗi vào tệp nhị phân, bạn cần nghĩ đến cách chuỗi sẽ được đọc. Có ba lựa chọn chính:
- Ghi chuỗi với ký tự null kết thúc ở cuối. Trong trường hợp này, chúng sẽ phải được phân tích từng ký tự trong một vòng lặp và kết hợp các ký tự thành chuỗi cho đến khi gặp số 0.
- Luôn ghi chuỗi có độ dài cố định (được xác định trước). Độ dài nên được chọn với một khoảng dự phòng cho hầu hết các kịch bản, hoặc theo đặc tả (yêu cầu kỹ thuật, giao thức, v.v.), nhưng điều này không kinh tế và không đảm bảo 100% rằng một chuỗi hiếm gặp nào đó sẽ không bị rút ngắn khi ghi vào tệp.
- Ghi độ dài dưới dạng số nguyên trước chuỗi.
Hàm
FileReadString
cũng có thể hoạt động với tệp văn bản. Khía cạnh này của ứng dụng của nó được mô tả trong phần tiếp theo.
Cũng lưu ý rằng nếu tham số size
là 0 (điều này có thể xảy ra trong một số phép tính), thì hàm không đọc: con trỏ tệp vẫn ở nguyên vị trí và hàm trả về một chuỗi rỗng.
Ví dụ cho phần này, chúng ta sẽ cải tiến tập lệnh FileStruct.mq5
từ phần trước. Tên chương trình mới là FileAtomic.mq5
.
Nhiệm vụ vẫn giữ nguyên: lưu một số lượng cấu trúc MqlRates đã cắt bớt với các báo giá vào tệp nhị phân. Nhưng bây giờ cấu trúc FileHeader
sẽ trở thành một lớp (và chữ ký định dạng sẽ được lưu trong một chuỗi, không phải trong mảng ký tự). Một tiêu đề kiểu này và một mảng báo giá sẽ là một phần của lớp điều khiển khác Candles
, và cả hai lớp sẽ được kế thừa từ giao diện Persistent
để ghi các đối tượng bất kỳ vào tệp và đọc từ tệp.
Đây là giao diện:
interface Persistent
{
bool write(int handle);
bool read(int handle);
};
2
3
4
5
Trong lớp FileHeader
, chúng ta sẽ triển khai việc lưu và kiểm tra chữ ký định dạng (hãy thay đổi nó thành "CANDLES/1.1") và các tên của biểu tượng hiện tại và khung thời gian biểu đồ (thêm về _Symbol
và _Period
tại đây).
Việc ghi được thực hiện trong triển khai của phương thức write
kế thừa từ giao diện.
class FileHeader : public Persistent
{
const string signature;
public:
FileHeader() : signature("CANDLES/1.1") { }
bool write(int handle) override
{
PRTF(FileWriteString(handle, signature, StringLen(signature)));
PRTF(FileWriteInteger(handle, StringLen(_Symbol), CHAR_VALUE));
PRTF(FileWriteString(handle, _Symbol));
PRTF(FileWriteString(handle, PeriodToString(), 3));
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
Chữ ký được ghi chính xác theo độ dài của nó vì mẫu được lưu trong đối tượng và độ dài tương tự sẽ được đặt khi đọc.
Đối với công cụ của biểu đồ hiện tại, chúng ta đầu tiên lưu độ dài tên của nó vào tệp (1 byte là đủ cho độ dài lên đến 255), và chỉ sau đó chúng ta lưu chính chuỗi đó.
Tên khung thời gian không bao giờ vượt quá 3 ký tự, nếu loại bỏ tiền tố hằng số "PERIOD_", do đó một độ dài cố định được chọn cho chuỗi này. Tên khung thời gian không có tiền tố được lấy từ hàm phụ trợ PeriodToString
: nó nằm trong tệp tiêu đề riêng Periods.mqh
(sẽ được thảo luận chi tiết hơn trong phần Biểu tượng và khung thời gian).
Việc đọc được thực hiện trong phương thức read
theo thứ tự ngược lại (tất nhiên, giả định rằng việc đọc sẽ được thực hiện trong một đối tượng mới, khác).
bool read(int handle) override
{
const string sig = PRTF(FileReadString(handle, StringLen(signature)));
if(sig != signature)
{
PrintFormat("Wrong file format, header is missing: want=%s vs got %s",
signature, sig);
return false;
}
const int len = PRTF(FileReadInteger(handle, CHAR_VALUE));
const string sym = PRTF(FileReadString(handle, len));
if(_Symbol != sym)
{
PrintFormat("Wrong symbol: file=%s vs chart=%s", sym, _Symbol);
return false;
}
const string stf = PRTF(FileReadString(handle, 3));
if(_Period != StringToPeriod(stf))
{
PrintFormat("Wrong timeframe: file=%s(%s) vs chart=%s",
stf, EnumToString(StringToPeriod(stf)), EnumToString(_Period));
return false;
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Nếu bất kỳ thuộc tính nào (chữ ký, biểu tượng, khung thời gian) không khớp trong tệp và trên biểu đồ hiện tại, hàm trả về false
để chỉ ra lỗi.
Việc chuyển đổi ngược tên khung thời gian thành liệt kê ENUM_TIMEFRAMES
được thực hiện bởi hàm StringToPeriod
, cũng từ tệp Periods.mqh
.
Lớp chính Candles
để yêu cầu, lưu và đọc kho lưu trữ báo giá như sau.
class Candles : public Persistent
{
FileHeader header;
int limit;
MqlRates rates[];
public:
Candles(const int size = 0) : limit(size)
{
if(size == 0) return;
int n = PRTF(CopyRates(_Symbol, _Period, 0, limit, rates));
if(n < 1)
{
limit = 0; // khởi tạo thất bại
}
limit = n; // có thể ít hơn yêu cầu
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Các trường bao gồm tiêu đề kiểu FileHeader
, số lượng thanh được yêu cầu limit
, và một mảng nhận các cấu trúc MqlRates
từ MetaTrader 5. Mảng được điền trong hàm tạo. Trong trường hợp lỗi, trường limit
được đặt lại về 0.
Là lớp dẫn xuất từ giao diện Persistent
, lớp Candles
yêu cầu triển khai các phương thức write
và read
. Trong phương thức write
, chúng ta đầu tiên hướng dẫn đối tượng tiêu đề tự lưu chính nó, sau đó thêm số lượng báo giá, phạm vi ngày (để tham khảo), và chính mảng đó vào tệp.
bool write(int handle) override
{
if(!limit) return false; // không có dữ liệu
if(!header.write(handle)) return false;
PRTF(FileWriteInteger(handle, limit));
PRTF(FileWriteLong(handle, rates[0].time));
PRTF(FileWriteLong(handle, rates[limit - 1].time));
for(int i = 0; i < limit; ++i)
{
FileWriteStruct(handle, rates[i], offsetof(MqlRates, tick_volume));
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
Việc đọc được thực hiện theo thứ tự ngược lại:
bool read(int handle) override
{
if(!header.read(handle))
{
return false;
}
limit = PRTF(FileReadInteger(handle));
ArrayResize(rates, limit);
ZeroMemory(rates);
// ngày cần được đọc: chúng không được sử dụng, nhưng điều này dịch chuyển vị trí trong tệp;
// có thể thay đổi vị trí rõ ràng, nhưng hàm này chưa được nghiên cứu
datetime dt0 = (datetime)PRTF(FileReadLong(handle));
datetime dt1 = (datetime)PRTF(FileReadLong(handle));
for(int i = 0; i < limit; ++i)
{
FileReadStruct(handle, rates[i], offsetof(MqlRates, tick_volume));
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Trong một chương trình thực tế để lưu trữ báo giá, sự hiện diện của phạm vi ngày sẽ cho phép xây dựng chuỗi chính xác của chúng qua lịch sử dài dựa trên các tiêu đề tệp và, ở một mức độ nào đó, bảo vệ chống lại việc đổi tên tệp tùy ý.
Có một phương thức print
đơn giản để kiểm soát quá trình:
void print() const
{
ArrayPrint(rates);
}
2
3
4
Trong hàm chính của tập lệnh, chúng ta tạo hai đối tượng Candles
, và sử dụng một trong số chúng, chúng ta đầu tiên lưu kho lưu trữ báo giá và sau đó khôi phục nó với sự trợ giúp của đối tượng còn lại. Các tệp được quản lý bởi lớp bao bọc FileHandle
mà chúng ta đã biết (xem phần Quản lý mô tả tệp).
const string filename = "MQL5Book/atomic.raw";
void OnStart()
{
// tạo tệp mới và đặt lại tệp cũ
FileHandle handle(PRTF(FileOpen(filename,
FILE_BIN | FILE_WRITE | FILE_ANSI | FILE_SHARE_READ)));
// hình thành dữ liệu
Candles output(BARLIMIT);
// ghi chúng vào tệp
if(!output.write(~handle))
{
Print("Can't write file");
return;
}
output.print();
// mở tệp vừa tạo để kiểm tra
handle = PRTF(FileOpen(filename,
FILE_BIN | FILE_READ | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE));
// tạo một đối tượng rỗng để nhận báo giá
Candles inputs;
// đọc dữ liệu từ tệp vào nó
if(!inputs.read(~handle))
{
Print("Can't read file");
}
else
{
inputs.print();
}
}
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
29
30
31
32
Dưới đây là ví dụ về nhật ký dữ liệu ban đầu cho XAUUSD,H1:
FileOpen(filename,FILE_BIN|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ)=1 / ok
CopyRates(_Symbol,_Period,0,limit,rates)=10 / ok
FileWriteString(handle,signature,StringLen(signature))=11 / ok
FileWriteInteger(handle,StringLen(_Symbol),CHAR_VALUE)=1 / ok
FileWriteString(handle,_Symbol)=6 / ok
FileWriteString(handle,PeriodToString(),3)=3 / ok
FileWriteInteger(handle,limit)=4 / ok
FileWriteLong(handle,rates[0].time)=8 / ok
FileWriteLong(handle,rates[limit-1].time)=8 / ok
[time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume]
[0] 2021.08.17 15:00:00 1791.40 1794.57 1788.04 1789.46 8157 5 0
[1] 2021.08.17 16:00:00 1789.46 1792.99 1786.69 1789.69 9285 5 0
[2] 2021.08.17 17:00:00 1789.76 1790.45 1780.95 1783.30 8165 5 0
[3] 2021.08.17 18:00:00 1783.30 1783.98 1780.53 1782.73 5114 5 0
[4] 2021.08.17 19:00:00 1782.69 1784.16 1782.09 1782.49 3586 6 0
[5] 2021.08.17 20:00:00 1782.49 1786.23 1782.17 1784.23 3515 5 0
[6] 2021.08.17 21:00:00 1784.20 1784.85 1782.73 1783.12 2627 6 0
[7] 2021.08.17 22:00:00 1783.10 1785.52 1782.37 1785.16 2114 5 0
[8] 2021.08.17 23:00:00 1785.11 1785.84 1784.71 1785.80 922 5 0
[9] 2021.08.18 01:00:00 1786.30 1786.34 1786.18 1786.20 13 5 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Và đây là ví dụ về dữ liệu đã khôi phục (nhớ rằng các cấu trúc được lưu ở dạng cắt bớt theo nhiệm vụ kỹ thuật giả định của chúng ta):
FileOpen(filename,FILE_BIN|FILE_READ|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE)=2 / ok
FileReadString(handle,StringLen(signature))=CANDLES/1.1 / ok
FileReadInteger(handle,CHAR_VALUE)=6 / ok
FileReadString(handle,len)=XAUUSD / ok
FileReadString(handle,3)=H1 / ok
FileReadInteger(handle)=10 / ok
FileReadLong(handle)=1629212400 / ok
FileReadLong(handle)=1629248400 / ok
[time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume]
[0] 2021.08.17 15:00:00 1791.40 1794.57 1788.04 1789.46 0 0 0
[1] 2021.08.17 16:00:00 1789.46 1792.99 1786.69 1789.69 0 0 0
[2] 2021.08.17 17:00:00 1789.76 1790.45 1780.95 1783.30 0 0 0
[3] 2021.08.17 18:00:00 1783.30 1783.98 1780.53 1782.73 0 0 0
[4] 2021.08.17 19:00:00 1782.69 1784.16 1782.09 1782.49 0 0 0
[5] 2021.08.17 20:00:00 1782.49 1786.23 1782.17 1784.23 0 0 0
[6] 2021.08.17 21:00:00 1784.20 1784.85 1782.73 1783.12 0 0 0
[7] 2021.08.17 22:00:00 1783.10 1785.52 1782.37 1785.16 0 0 0
[8] 2021.08.17 23:00:00 1785.11 1785.84 1784.71 1785.80 0 0 0
[9] 2021.08.18 01:00:00 1786.30 1786.34 1786.18 1786.20 0 0 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Có thể dễ dàng kiểm tra rằng dữ liệu được lưu và đọc chính xác. Và bây giờ hãy xem chúng trông như thế nào bên trong tệp:
Chú thích: Xem cấu trúc bên trong của tệp nhị phân với kho lưu trữ báo giá trong chương trình bên ngoài
Ở đây, các trường khác nhau của tiêu đề của chúng ta được tô sáng bằng màu sắc: chữ ký, độ dài tên biểu tượng, tên biểu tượng, tên khung thời gian, v.v.