Quản lý mô tả tệp
Vì chúng ta cần liên tục ghi nhớ về các tệp đang mở và giải phóng các mô tả cục bộ khi thoát khỏi bất kỳ hàm nào, việc giao phó toàn bộ quy trình này cho các đối tượng đặc biệt sẽ hiệu quả hơn.
Cách tiếp cận này rất nổi tiếng trong lập trình và được gọi là Resource Acquisition Is Initialization (RAII) (Khởi tạo là Thu nhận Tài nguyên). Sử dụng RAII giúp việc kiểm soát tài nguyên dễ dàng hơn và đảm bảo chúng ở trạng thái chính xác. Đặc biệt, điều này rất hiệu quả nếu hàm mở tệp (và tạo một đối tượng sở hữu cho nó) thoát ra từ nhiều vị trí khác nhau.
Phạm vi của RAII không chỉ giới hạn ở các tệp. Trong phần Mẫu kiểu đối tượng, chúng ta đã tạo lớp AutoPtr
, lớp này quản lý một con trỏ tới một đối tượng. Đây là một ví dụ khác của khái niệm này, vì con trỏ cũng là một tài nguyên (bộ nhớ), và rất dễ mất nó cũng như tốn tài nguyên để giải phóng nó trong nhiều nhánh khác nhau của thuật toán.
Một lớp bao bọc tệp cũng có thể hữu ích theo cách khác. API tệp không cung cấp hàm nào cho phép bạn lấy tên của tệp từ một mô tả (mặc dù mối quan hệ này chắc chắn tồn tại bên trong nội bộ). Đồng thời, bên trong đối tượng, chúng ta có thể lưu trữ tên này và tự thực hiện việc gắn kết với mô tả.
Trong trường hợp đơn giản nhất, chúng ta cần một lớp lưu trữ mô tả tệp và tự động đóng nó trong hàm hủy. Một ví dụ triển khai được thể hiện trong tệp FileHandle.mqh
.
class FileHandle
{
int handle;
public:
FileHandle(const int h = INVALID_HANDLE) : handle(h)
{
}
FileHandle(int &holder, const int h) : handle(h)
{
holder = h;
}
int operator=(const int h)
{
handle = h;
return h;
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Hai hàm tạo, cùng với một toán tử gán được nạp chồng, đảm bảo rằng một đối tượng được gắn với một tệp (mô tả). Hàm tạo thứ hai cho phép bạn truyền một tham chiếu tới một biến cục bộ (từ mã gọi), biến này sẽ nhận thêm một mô tả mới. Đây sẽ là một dạng bí danh bên ngoài cho cùng một mô tả, có thể được sử dụng theo cách thông thường trong các lời gọi hàm khác.
Nhưng bạn cũng có thể không cần bí danh. Đối với những trường hợp này, lớp định nghĩa toán tử ~
, trả về giá trị của biến nội bộ handle
.
int operator~() const
{
return handle;
}
2
3
4
Cuối cùng, điều quan trọng nhất mà lớp này được triển khai là hàm hủy thông minh:
~FileHandle()
{
if(handle != INVALID_HANDLE)
{
ResetLastError();
// sẽ đặt mã lỗi nội bộ nếu mô tả không hợp lệ
FileGetInteger(handle, FILE_SIZE);
if(_LastError == 0)
{
#ifdef FILE_DEBUG_PRINT
Print(__FUNCTION__, ": Automatic close for handle: ", handle);
#endif
FileClose(handle);
}
else
{
PrintFormat("%s: handle %d is incorrect, %s(%d)",
__FUNCTION__, handle, E2S(_LastError), _LastError);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Trong đó, sau một số kiểm tra, FileClose
được gọi cho biến handle
được kiểm soát. Vấn đề là tệp có thể đã được đóng rõ ràng ở nơi khác trong chương trình, mặc dù điều này không còn cần thiết với lớp này. Kết quả là, mô tả có thể trở nên không hợp lệ vào thời điểm hàm hủy được gọi khi quá trình thực thi thuật toán rời khỏi khối mà đối tượng FileHandle
được định nghĩa. Để tìm hiểu điều này, một lời gọi giả tới hàm FileGetInteger
được sử dụng. Nó là giả vì nó không thực hiện bất cứ điều gì hữu ích. Nếu mã lỗi nội bộ vẫn là 0 sau lời gọi, mô tả là hợp lệ.
Chúng ta có thể bỏ qua tất cả các kiểm tra này và chỉ đơn giản viết như sau:
~FileHandle()
{
if(handle != INVALID_HANDLE)
{
FileClose(handle);
}
}
2
3
4
5
6
7
Nếu mô tả bị hỏng, FileClose
sẽ không trả về bất kỳ cảnh báo nào. Nhưng chúng ta đã thêm các kiểm tra để có thể xuất thông tin chẩn đoán.
Hãy thử lớp FileHandle
trong thực tế. Script thử nghiệm cho nó được gọi là FileHandle.mq5
.
const string dummy = "MQL5Book/dummy";
void OnStart()
{
// tạo tệp mới hoặc mở tệp hiện có và đặt lại nó
FileHandle fh1(PRTF(FileOpen(dummy,
FILE_TXT | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ))); // 1
// một cách khác để kết nối mô tả qua '='
int h = PRTF(FileOpen(dummy,
FILE_TXT | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ)); // 2
FileHandle fh2 = h;
// và cú pháp được hỗ trợ khác:
// int f;
// FileHandle ff(f, FileOpen(dummy,
// FILE_TXT | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
// dữ liệu được cho là sẽ được ghi ở đây
// ...
// đóng tệp thủ công (điều này không cần thiết; chỉ thực hiện để chứng minh
// rằng FileHandle sẽ phát hiện điều này và không cố đóng lại)
FileClose(~fh1); // toán tử '~' áp dụng cho một đối tượng trả về một mô tả
// mô tả trong biến 'h' gắn với đối tượng 'fh2' không được đóng thủ công
// và sẽ được tự động đóng trong hàm hủy
}
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
Theo đầu ra trong nhật ký, mọi thứ hoạt động như kế hoạch:
FileHandle::~FileHandle: Automatic close for handle: 2
FileHandle::~FileHandle: handle 1 is incorrect, INVALID_FILEHANDLE(5007)
2
Tuy nhiên, nếu có nhiều tệp, việc tạo một bản sao đối tượng theo dõi cho mỗi tệp có thể trở nên bất tiện. Trong những tình huống như vậy, việc thiết kế một đối tượng duy nhất thu thập tất cả các mô tả trong một ngữ cảnh nhất định (ví dụ, bên trong một hàm) sẽ hợp lý hơn.
Lớp như vậy được triển khai trong tệp FileHolder.mqh
và được hiển thị trong script FileHolder.mq5
. Một bản sao của FileHolder
tự tạo ra các đối tượng quan sát phụ trợ của lớp FileOpener
theo yêu cầu, lớp này chia sẻ các đặc điểm chung với FileHandle
, đặc biệt là hàm hủy, cũng như trường handle
.
Để mở một tệp qua FileHolder
, bạn nên sử dụng phương thức FileOpen
của nó (chữ ký của nó lặp lại chữ ký của hàm FileOpen
tiêu chuẩn).
class FileHolder
{
static FileOpener *files[];
int expand()
{
return ArrayResize(files, ArraySize(files) + 1) - 1;
}
public:
int FileOpen(const string filename, const int flags,
const ushort delimiter = '\t', const uint codepage = CP_ACP)
{
const int n = expand();
if(n > -1)
{
files[n] = new FileOpener(filename, flags, delimiter, codepage);
return files[n].handle;
}
return INVALID_HANDLE;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Tất cả các đối tượng FileOpener
được thêm vào mảng files
để theo dõi vòng đời của chúng. Tại cùng vị trí đó, các phần tử bằng 0 đánh dấu các thời điểm đăng ký các ngữ cảnh cục bộ (các khối mã) mà trong đó các đối tượng FileHolder
được tạo. Hàm tạo của FileHolder
chịu trách nhiệm cho việc này.
FileHolder()
{
const int n = expand();
if(n > -1)
{
files[n] = NULL;
}
}
2
3
4
5
6
7
8
Như chúng ta biết, trong quá trình thực thi một chương trình, nó đi vào các khối mã lồng nhau (nó gọi các hàm). Nếu chúng yêu cầu quản lý các mô tả tệp cục bộ, các đối tượng FileHolder
(một cho mỗi khối hoặc ít hơn) nên được mô tả ở đó. Theo quy tắc của ngăn xếp (vào trước, ra sau), tất cả các mô tả như vậy được thêm vào files
và sau đó được giải phóng theo thứ tự ngược lại khi chương trình rời khỏi các ngữ cảnh. Hàm hủy được gọi tại mỗi thời điểm như vậy.
~FileHolder()
{
for(int i = ArraySize(files) - 1; i >= 0; --i)
{
if(files[i] == NULL)
{
// giảm kích thước mảng và thoát
ArrayResize(files, i);
return;
}
delete files[i];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Nhiệm vụ của nó là xóa các đối tượng FileOpener
cuối cùng trong mảng cho đến khi gặp phần tử 0 đầu tiên, phần tử này chỉ ra ranh giới của ngữ cảnh (xa hơn trong mảng là các mô tả từ một ngữ cảnh bên ngoài khác).
Bạn có thể tự nghiên cứu toàn bộ lớp này.
Hãy xem cách sử dụng nó trong script thử nghiệm FileHolder.mq5
. Ngoài hàm OnStart
, nó còn có SubFunc
. Các thao tác với tệp được thực hiện trong cả hai ngữ cảnh.
const string dummy = "MQL5Book/dummy";
void SubFunc()
{
Print(__FUNCTION__, " enter");
FileHolder holder;
int h = PRTF(holder.FileOpen(dummy,
FILE_BIN | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
int f = PRTF(holder.FileOpen(dummy,
FILE_BIN | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
// sử dụng h và f
// ...
// không cần đóng tệp thủ công và theo dõi việc thoát hàm sớm
Print(__FUNCTION__, " exit");
}
void OnStart()
{
Print(__FUNCTION__, " enter");
FileHolder holder;
int h = PRTF(holder.FileOpen(dummy,
FILE_BIN | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
// ghi dữ liệu và các hành động khác trên tệp bằng mô tả
// ...
/*
int a[] = {1, 2, 3};
FileWriteArray(h, a);
*/
SubFunc();
SubFunc();
if(rand() > 32000) // mô phỏng phân nhánh theo điều kiện
{
// nhờ holder chúng ta không cần gọi rõ ràng
// FileClose(h);
Print(__FUNCTION__, " return");
return; // có thể có nhiều lối thoát từ hàm
}
/*
... thêm mã
*/
// nhờ holder chúng ta không cần gọi rõ ràng
// FileClose(h);
Print(__FUNCTION__, " exit");
}
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Chúng ta không đóng bất kỳ mô tả nào theo cách thủ công, các phiên bản của FileHolder
sẽ tự động thực hiện điều đó trong các hàm hủy.
Dưới đây là một ví dụ về đầu ra nhật ký:
OnStart enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=1 / ok
SubFunc enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=2 / ok
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=3 / ok
SubFunc exit
FileOpener::~FileOpener: Automatic close for handle: 3
FileOpener::~FileOpener: Automatic close for handle: 2
SubFunc enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=2 / ok
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=3 / ok
SubFunc exit
FileOpener::~FileOpener: Automatic close for handle: 3
FileOpener::~FileOpener: Automatic close for handle: 2
OnStart exit
FileOpener::~FileOpener: Automatic close for handle: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16