Mở và đóng tệp
Để ghi và đọc dữ liệu từ một tệp, hầu hết các hàm MQL5 yêu cầu tệp phải được mở trước. Với mục đích này, có hàm FileOpen
. Sau khi thực hiện các thao tác cần thiết, tệp đã mở nên được đóng bằng hàm FileClose
. Thực tế là một tệp đang mở có thể, tùy thuộc vào các tùy chọn được áp dụng, bị chặn truy cập từ các chương trình khác. Ngoài ra, các thao tác tệp được lưu vào bộ nhớ đệm (cache) vì lý do hiệu suất, và nếu không đóng tệp, dữ liệu mới có thể không được tải vật lý lên tệp trong một khoảng thời gian. Điều này đặc biệt quan trọng nếu dữ liệu đang ghi được chờ đợi bởi một chương trình bên ngoài (ví dụ, khi tích hợp chương trình MQL với các hệ thống khác). Chúng ta tìm hiểu về một cách thay thế để xóa bộ đệm xuống đĩa từ mô tả của hàm FileFlush
.
Một số nguyên đặc biệt được gọi là mô tả (descriptor) được liên kết với một tệp đã mở trong chương trình MQL. Nó được trả về bởi hàm FileOpen
. Tất cả các thao tác liên quan đến truy cập hoặc sửa đổi nội dung bên trong của tệp yêu cầu chỉ định định danh này trong các hàm API tương ứng. Những hàm hoạt động trên toàn bộ tệp (sao chép, xóa, di chuyển, kiểm tra sự tồn tại) không yêu cầu mô tả. Bạn không cần mở tệp để thực hiện các bước này.
int FileOpen(const string filename, int flags, const short delimiter = '\t', uint codepage = CP_ACP)
int FileOpen(const string filename, int flags, const string delimiter, uint codepage = CP_ACP)
Hàm này mở một tệp với tên được chỉ định, trong chế độ được xác định bởi tham số flags
. Tham số filename
có thể chứa các thư mục con trước tên tệp thực tế. Trong trường hợp này, nếu tệp được mở để ghi và hệ thống thư mục cần thiết chưa tồn tại, nó sẽ được tạo ra.
Tham số flags
phải chứa một tổ hợp các hằng số mô tả chế độ làm việc cần thiết với tệp. Tổ hợp này được thực hiện bằng các phép toán bitwise OR. Dưới đây là bảng các hằng số có sẵn:
Định danh | Giá trị | Mô tả |
---|---|---|
FILE_READ | 1 | Tệp được mở để đọc |
FILE_WRITE | 2 | Tệp được mở để ghi |
FILE_BIN | 4 | Chế độ đọc-ghi nhị phân, không chuyển đổi dữ liệu từ chuỗi sang chuỗi |
FILE_CSV | 8 | Tệp loại CSV; dữ liệu được ghi sẽ chuyển thành văn bản của kiểu tương ứng (Unicode hoặc ANSI, xem dưới đây), và khi đọc, thực hiện chuyển đổi ngược từ văn bản sang kiểu cần thiết (được chỉ định trong hàm đọc); một bản ghi CSV là một dòng văn bản, phân cách bằng ký tự xuống dòng (thường là CRLF); bên trong bản ghi CSV, các phần tử được phân cách bằng ký tự phân tách (tham số delimiter ) |
FILE_TXT | 16 | Tệp văn bản thuần túy, tương tự chế độ CSV, nhưng không sử dụng ký tự phân tách (giá trị của tham số delimiter bị bỏ qua) |
FILE_ANSI | 32 | Chuỗi kiểu ANSI (ký tự một byte) |
FILE_UNICODE | 64 | Chuỗi kiểu Unicode (ký tự hai byte) |
FILE_SHARE_READ | 128 | Truy cập đọc chung từ nhiều chương trình |
FILE_SHARE_WRITE | 256 | Truy cập ghi chung từ nhiều chương trình |
FILE_REWRITE | 512 | Cho phép ghi đè tệp (nếu đã tồn tại) trong các hàm FileCopy và FileMove |
FILE_COMMON | 4096 | Vị trí tệp trong thư mục chung của tất cả các terminal khách hàng /Terminal/Common/Files (cờ này được sử dụng khi mở tệp (FileOpen ), sao chép tệp (FileCopy , FileMove ) và kiểm tra sự tồn tại của tệp (FileIsExist )) |
Khi mở một tệp, một trong các cờ FILE_WRITE
, FILE_READ
hoặc tổ hợp của chúng phải được chỉ định.
Cờ FILE_SHARE_READ
và FILE_SHARE_WRITE
không thay thế hoặc hủy bỏ nhu cầu chỉ định cờ FILE_READ
và FILE_WRITE
.
Môi trường thực thi chương trình MQL luôn lưu trữ tệp vào bộ đệm để đọc, điều này tương đương với việc tự động thêm cờ
FILE_READ
. Vì lý do này, nên luôn sử dụngFILE_SHARE_READ
để làm việc đúng với các tệp chia sẻ (ngay cả khi biết rằng một quá trình khác chỉ mở tệp để ghi).
Nếu không chỉ định bất kỳ cờ nào trong số FILE_CSV
, FILE_BIN
, FILE_TXT
, thì FILE_CSV
được giả định là có độ ưu tiên cao nhất. Nếu chỉ định nhiều hơn một trong ba cờ này, cờ có độ ưu tiên cao nhất được áp dụng (chúng được liệt kê ở trên theo thứ tự ưu tiên giảm dần).
Đối với các tệp văn bản, chế độ mặc định là FILE_UNICODE
.
Tham số delimiter
chỉ ảnh hưởng đến CSV, có thể thuộc kiểu ushort
hoặc string
. Trong trường hợp thứ hai, nếu độ dài chuỗi lớn hơn 1, chỉ ký tự đầu tiên của nó sẽ được sử dụng.
Tham số codepage
chỉ ảnh hưởng đến các tệp được mở ở chế độ văn bản (FILE_TXT
hoặc FILE_CSV
), và chỉ khi chế độ FILE_ANSI
được chọn cho chuỗi. Nếu chuỗi được lưu trữ ở Unicode (FILE_UNICODE
), trang mã không quan trọng.
Nếu thành công, hàm trả về một mô tả tệp, là một số nguyên dương. Nó chỉ duy nhất trong một chương trình MQL cụ thể; việc chia sẻ nó với các chương trình khác không có ý nghĩa. Để tiếp tục làm việc với tệp, mô tả này được truyền vào các lời gọi của các hàm khác.
Khi có lỗi, kết quả là INVALID_HANDLE
(-1). Bản chất của lỗi cần được làm rõ từ mã trả về bởi hàm GetLastError
.
Tất cả các cài đặt chế độ hoạt động được thực hiện khi tệp được mở sẽ giữ nguyên trong suốt thời gian tệp còn mở. Nếu cần thay đổi chế độ, tệp nên được đóng và mở lại với các tham số mới.
Đối với mỗi tệp đang mở, môi trường thực thi chương trình MQL duy trì một con trỏ nội bộ, tức là vị trí hiện tại trong tệp. Ngay sau khi mở tệp, con trỏ được đặt ở đầu (vị trí 0). Trong quá trình ghi hoặc đọc, vị trí được dịch chuyển phù hợp, theo lượng dữ liệu được truyền hoặc nhận từ các hàm tệp khác nhau. Cũng có thể ảnh hưởng trực tiếp đến vị trí (di chuyển ngược hoặc tiến). Tất cả các cơ hội này sẽ được thảo luận trong các phần tiếp theo.
FILE_READ
và FILE_WRITE
trong các tổ hợp khác nhau cho phép đạt được một số kịch bản:
FILE_READ
— mở tệp chỉ khi nó tồn tại; nếu không, hàm trả về lỗi và không tạo tệp mới.FILE_WRITE
— tạo tệp mới nếu nó chưa tồn tại, hoặc mở tệp hiện có, và nội dung của nó bị xóa và kích thước được đặt lại về 0.FILE_READ|FILE_WRITE
— mở tệp hiện có với toàn bộ nội dung của nó hoặc tạo tệp mới nếu nó chưa tồn tại.
Như bạn thấy, một số kịch bản không thể truy cập chỉ bằng cờ. Đặc biệt, bạn không thể mở tệp để ghi chỉ khi nó đã tồn tại. Điều này có thể đạt được bằng các hàm bổ sung, ví dụ, FileIsExist
. Ngoài ra, không thể "tự động" đặt lại một tệp được mở cho tổ hợp đọc và ghi: trong trường hợp này, MQL5 luôn giữ lại nội dung.
Để thêm dữ liệu vào tệp, không chỉ cần mở tệp ở chế độ FILE_READ|FILE_WRITE
, mà còn phải di chuyển vị trí hiện tại trong tệp đến cuối bằng cách gọi FileSeek
.
Mô tả chính xác về quyền truy cập chia sẻ vào tệp là điều kiện tiên quyết để thực hiện thành công FileOpen
. Khía cạnh này được quản lý như sau:
- Nếu không chỉ định cờ
FILE_SHARE_READ
vàFILE_SHARE_WRITE
, thì chương trình hiện tại nhận được quyền truy cập độc quyền vào tệp nếu nó mở tệp đầu tiên. Nếu tệp đã được mở trước đó bởi ai đó (bởi chương trình khác hoặc cùng chương trình), lời gọi hàm sẽ thất bại. - Khi đặt cờ
FILE_SHARE_READ
, chương trình cho phép các yêu cầu tiếp theo mở cùng tệp để đọc. Nếu tại thời điểm gọi hàm, tệp đã được mở để đọc bởi chương trình khác hoặc cùng chương trình mà cờ này không được đặt, hàm sẽ thất bại. - Khi đặt cờ
FILE_SHARE_WRITE
, chương trình cho phép các yêu cầu tiếp theo mở cùng tệp để ghi. Nếu tại thời điểm gọi hàm, tệp đã được mở để ghi bởi chương trình khác hoặc cùng chương trình mà cờ này không được đặt, hàm sẽ thất bại.
Việc chia sẻ truy cập được kiểm tra không chỉ đối với các chương trình MQL khác hoặc các quá trình bên ngoài MetaTrader 5, mà còn đối với cùng một chương trình MQL nếu nó mở lại tệp.
Do đó, chế độ ít xung đột nhất ngụ ý rằng cả hai cờ đều được chỉ định, nhưng điều này vẫn không đảm bảo rằng tệp sẽ được mở nếu ai đó đã được cấp mô tả cho nó mà không có quyền chia sẻ. Tuy nhiên, các quy tắc nghiêm ngặt hơn nên được tuân thủ tùy thuộc vào kế hoạch đọc hoặc ghi.
Ví dụ, khi mở tệp để đọc, có thể hợp lý để giữ cơ hội cho người khác đọc nó. Ngoài ra, bạn có thể cho phép người khác ghi vào nó, nếu đó là tệp đang được bổ sung (ví dụ, một nhật ký). Tuy nhiên, khi mở tệp để ghi, khó có thể để lại quyền ghi cho người khác: điều này sẽ dẫn đến việc dữ liệu bị chồng lấp không thể dự đoán.
void FileClose(int handle)
Hàm này đóng một tệp đã mở trước đó bằng mô tả của nó.
Sau khi tệp được đóng, mô tả của nó trong chương trình trở nên không hợp lệ: mọi nỗ lực gọi bất kỳ hàm tệp nào trên nó sẽ dẫn đến lỗi. Tuy nhiên, bạn có thể sử dụng cùng biến để lưu trữ một mô tả khác nếu bạn mở lại cùng tệp hoặc một tệp khác.
Khi chương trình kết thúc, các tệp đang mở sẽ bị đóng buộc, và bộ đệm ghi, nếu không rỗng, sẽ được ghi xuống đĩa. Tuy nhiên, khuyến nghị là đóng tệp một cách rõ ràng.
Việc đóng tệp khi bạn hoàn tất làm việc với nó là một quy tắc quan trọng cần tuân theo. Điều này không chỉ liên quan đến việc lưu trữ thông tin đang ghi, có thể vẫn nằm trong RAM một thời gian và không được lưu xuống đĩa (như đã đề cập ở trên), nếu tệp không được đóng. Ngoài ra, một tệp đang mở tiêu tốn một số tài nguyên nội bộ của hệ điều hành, và chúng ta không nói về không gian đĩa. Số lượng tệp mở đồng thời bị giới hạn (có thể là vài trăm hoặc hàng nghìn tùy thuộc vào cài đặt Windows). Nếu nhiều chương trình giữ một số lượng lớn tệp mở, giới hạn này có thể đạt tới và các nỗ lực mở tệp mới sẽ thất bại.
Về vấn đề này, nên bảo vệ bản thân khỏi khả năng mất mô tả bằng cách sử dụng một lớp bao bọc sẽ mở tệp và nhận mô tả khi tạo đối tượng, và mô tả sẽ được giải phóng và tệp sẽ tự động đóng trong hàm hủy.
Chúng ta sẽ tạo một lớp bao bọc sau khi thử nghiệm các hàm FileOpen
và FileClose
thuần túy.
Nhưng trước khi đi sâu vào chi tiết tệp, hãy chuẩn bị một phiên bản mới của macro để minh họa đầu ra của các hàm của chúng ta vào nhật ký gọi. Phiên bản mới được yêu cầu vì cho đến nay, các macro như PRT
và PRTS
(được sử dụng trong các phần trước) "hấp thụ" giá trị trả về của hàm trong khi in. Ví dụ, chúng ta đã viết:
PRT(FileLoad(filename, read));
Ở đây, kết quả của lời gọi FileLoad
được gửi đến nhật ký, nhưng không thể lấy nó trong chuỗi mã gọi. Thành thật mà nói, chúng ta không cần nó. Nhưng bây giờ hàm FileOpen
sẽ trả về một mô tả tệp, và nó nên được lưu trữ trong một biến để tiếp tục thao tác với tệp.
Có hai vấn đề với các macro cũ. Thứ nhất, chúng dựa trên hàm Print
, hàm này tiêu thụ dữ liệu được truyền (gửi nó đến nhật ký) nhưng bản thân nó không trả về gì. Thứ hai, bất kỳ giá trị nào cho một biến với kết quả chỉ có thể được lấy từ một biểu thức, và lời gọi Print
không thể trở thành một phần của biểu thức do nó có kiểu void
.
Để giải quyết các vấn đề này, chúng ta cần một hàm trợ giúp in trả về giá trị có thể in. Và chúng ta sẽ đóng gói lời gọi của nó vào một macro PRTF
mới:
#include <MQL5Book/MqlError.mqh>
#define PRTF(A) ResultPrint(#A, (A))
template<typename T>
T ResultPrint(const string s, const T retval = 0)
{
const string err = E2S(_LastError) + "(" + (string)_LastError + ")";
Print(s, "=", retval, " / ", (_LastError == 0 ? "ok" : err));
ResetLastError(); // xóa cờ lỗi cho lời gọi tiếp theo
return retval;
}
2
3
4
5
6
7
8
9
10
11
12
Sử dụng toán tử chuyển đổi chuỗi kỳ diệu #
, chúng ta nhận được một mô tả chi tiết của đoạn mã (biểu thức A) được truyền làm đối số đầu tiên cho ResultPrint
. Bản thân biểu thức (đối số macro) được đánh giá (nếu có hàm, nó được gọi), và kết quả của nó được truyền làm đối số thứ hai cho ResultPrint
. Tiếp theo, hàm Print
thông thường hoạt động, và cuối cùng, cùng kết quả đó được trả về mã gọi.
Để không phải tra cứu trong Trợ giúp để giải mã mã lỗi, một macro E2S
đã được chuẩn bị sử dụng liệt kê MQL_ERROR
với tất cả các lỗi MQL5. Nó có thể được tìm thấy trong tệp tiêu đề MQL5/Include/MQL5Book/MqlError.mqh
. Macro mới và hàm ResultPrint
được định nghĩa trong tệp PRTF.mqh
, bên cạnh các script thử nghiệm.
Trong script FileOpenClose.mq5
, hãy thử mở các tệp khác nhau, và đặc biệt, cùng một tệp sẽ được mở nhiều lần song song. Điều này thường được tránh trong các chương trình thực tế. Một mô tả duy nhất cho một tệp cụ thể trong một phiên bản chương trình là đủ cho hầu hết các nhiệm vụ.
Một trong các tệp, MQL5Book/rawdata
, phải đã tồn tại vì nó được tạo bởi script từ phần Ghi và đọc tệp trong chế độ đơn giản. Một tệp khác sẽ được tạo trong quá trình thử nghiệm.
Chúng ta sẽ chọn loại tệp FILE_BIN
. Làm việc với FILE_TXT
hoặc FILE_CSV
sẽ tương tự ở giai đoạn này.
Hãy dự trữ một mảng cho các mô tả tệp để cuối script chúng ta đóng tất cả tệp cùng một lúc.
Đầu tiên, hãy mở MQL5Book/rawdata
ở chế độ đọc mà không chia sẻ truy cập. Giả sử rằng tệp không được sử dụng bởi bất kỳ ứng dụng bên thứ ba nào, chúng ta có thể mong đợi nhận được mô tả thành công.
void OnStart()
{
int ha[4] = {}; // mảng cho các mô tả tệp thử nghiệm
// tệp này phải tồn tại sau khi chạy FileSaveLoad.mq5
const string rawdata = "MQL5Book/rawdata";
ha[0] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ)); // 1 / ok
2
3
4
5
6
7
Nếu chúng ta cố mở lại cùng tệp, chúng ta sẽ gặp lỗi vì cả lời gọi đầu tiên lẫn thứ hai đều không cho phép chia sẻ.
ha[1] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ)); // -1 / CANNOT_OPEN_FILE(5004)
Hãy đóng mô tả đầu tiên, mở lại tệp, nhưng với quyền đọc chia sẻ, và đảm bảo rằng việc mở lại giờ đây hoạt động (mặc dù nó cũng cần cho phép đọc chia sẻ):
FileClose(ha[0]);
ha[0] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ | FILE_SHARE_READ)); // 1 / ok
ha[1] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ | FILE_SHARE_READ)); // 2 / ok
2
3
Việc mở tệp để ghi (FILE_WRITE
) sẽ không hoạt động, vì hai lời gọi trước của FileOpen
chỉ cho phép FILE_SHARE_READ
.
ha[2] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ | FILE_WRITE | FILE_SHARE_READ));
// -1 / CANNOT_OPEN_FILE(5004)
2
Bây giờ hãy thử tạo một tệp mới MQL5Book/newdata
. Nếu bạn mở nó chỉ để đọc, tệp sẽ không được tạo.
const string newdata = "MQL5Book/newdata";
ha[3] = PRTF(FileOpen(newdata, FILE_BIN | FILE_READ));
// -1 / CANNOT_OPEN_FILE(5004)
2
3
Để tạo tệp, bạn phải chỉ định chế độ FILE_WRITE
(sự hiện diện của FILE_READ
không quan trọng ở đây, nhưng nó làm cho lời gọi trở nên linh hoạt hơn: như chúng ta nhớ, trong tổ hợp này, hướng dẫn đảm bảo rằng hoặc tệp cũ sẽ được mở nếu nó tồn tại, hoặc một tệp mới sẽ được tạo).
ha[3] = PRTF(FileOpen(newdata, FILE_BIN | FILE_READ | FILE_WRITE)); // 3 / ok
Hãy thử ghi một thứ gì đó vào tệp mới bằng hàm FileSave
mà chúng ta đã biết. Nó hoạt động như một "người chơi bên ngoài", vì nó làm việc với tệp mà không qua mô tả của chúng ta, tương tự như cách một chương trình MQL khác hoặc ứng dụng bên thứ ba có thể làm.
long x[1] = {0x123456789ABCDEF0};
PRTF(FileSave(newdata, x)); // false
2
Lời gọi này thất bại vì mô tả được mở mà không có quyền chia sẻ. Đóng và mở lại tệp với "quyền tối đa".
FileClose(ha[3]);
ha[3] = PRTF(FileOpen(newdata,
FILE_BIN | FILE_READ | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE)); // 3 / ok
2
3
Lần này FileSave
hoạt động như kỳ vọng.
PRTF(FileSave(newdata, x)); // true
Bạn có thể nhìn vào thư mục MQL5/Files/MQL5Book/
và tìm thấy tệp newdata
, dài 8 byte.
Lưu ý rằng sau khi chúng ta đóng tệp, mô tả của nó được trả về nhóm mô tả tự do, và lần tiếp theo khi mở một tệp (có thể là tệp khác), cùng số đó lại được sử dụng.
Để tắt máy gọn gàng, chúng ta sẽ đóng tất cả các tệp đang mở một cách rõ ràng.
for(int i = 0; i < ArraySize(ha); ++i)
{
if(ha[i] != INVALID_HANDLE)
{
FileClose(ha[i]);
}
}
}
2
3
4
5
6
7
8