打开和关闭文件
要从文件读写数据,大多数 MQL5 函数要求首先打开该文件。为此,提供了 FileOpen 函数。执行要求的操作后,应使用 FileClose 函数关闭打开的文件。事实是,根据应用的选项,打开的文件可能被阻止从其它程序访问。此外,出于性能考虑,文件操作会在内存(缓存)中进行缓冲,如果不关闭文件,新数据在一定时间内可能不会实际上传到该文件。如果要写入的数据正在等待外部程序(例如,将 MQL 程序与其它系统集成时),这一点尤其关键。根据 FileFlush 函数的描述,我们可以了解一种将缓冲区刷写到磁盘的替代方法。
在 MQL 程序中,一种称为描述符的特殊整数与打开的文件关联。这个整数由 FileOpen 函数返回。与访问或修改文件的内部内容相关的所有操作均需要在对应的 API 函数中指定该标识符。对整个文件进行操作(复制、删除、移动、检查存在性)的函数不需要描述符。你不需要打开文件就能执行这些步骤。
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)
该函数以 flags 参数指定的模式打开具有指定名称的文件。filename 参数可能在实际文件名之前包含子文件夹。在此情况下,如果文件被打开以进行写入并且要求的文件夹层次不存在,则将创建该文件夹层次。
flags 参数必须包含描述处理该文件所需模式的常量组合。组合使用 按位“或”的运算来执行。下面是可用常量表。
标识符 |
值 |
说明 |
---|---|---|
FILE_READ |
1 |
打开文件进行读取 |
FILE_WRITE |
2 |
打开文件进行写入 |
FILE_BIN |
4 |
二进制读写模式,无需字符串到字符串的数据转换 |
FILE_CSV |
8 |
CSV 类型文件;要写入的数据被转换为适当类型(Unicode 或 ANSI,见下文)的文本,读取时,执行从文本到所要求类型(在读取函数中指定)的逆转换;一条 CSV 记录对应一行文本,以换行字符(通常是 CRLF)分隔;在 CSV 记录内,元素以定界符字符(参数 delimiter)分隔; |
FILE_TXT |
16 |
纯文本文件,类似于 CSV 模式,但未使用定界符字符(delimiter 参数的值被忽略) |
FILE_ANSI |
32 |
ANSI 类型字符串(单字节字符) |
FILE_UNICODE |
64 |
Unicode 类型字符串(双字节字符) |
FILE_SHARE_READ |
128 |
若干个程序共享的读取访问 |
FILE_SHARE_WRITE |
256 |
多个程序共享的写入访问 |
FILE_REWRITE |
512 |
|
FILE_COMMON |
4096 |
所有客户端终端的共享文件夹中的文件位置 /Terminal/Common/Files(当打开文件 (FileOpen)、复制文件(FileCopy 和 FileMove)以及检查文件存在性 (FileIsExist) 时,使用标志) |
当打开一个文件时,必须指定 FILE_WRITE、FILE_READ 标志其组合之一。
即使有了 FILE_SHARE_READ 和 FILE_SHARE_WRITE 标志,也不意味着不需要指定 FILE_READ 和 FILE_WRITE 标志。
MQL 程序执行环境始终会对要读取的文件进行缓冲处理,这等同于隐式添加 FILE_READ 标志。因此,FILE_SHARE_READ 应始终用于正确处理共享文件(即使已知另一个进程打开了一个只写文件)。
如果没有指定 FILE_CSV、FILE_BIN 和 FILE_TXT 标志,则以 FILE_CSV 作为最高优先级。如果至少指定了这三个标志中的两个,则会应用传递的最高优先级(它们在上面以降序优先级列出)。
对于文本文件,默认模式为 FILE_UNICODE。
仅影响 CSV 的 delimiter 参数可以是 ushort 或 string 类型。在第二种情况下,如果字符串长度大于 1,则将仅使用其第一个字符。
codepage 参数仅影响以文本模式(FILE_TXT 或 FILE_CSV)打开的文件,且仅当为字符串选择了 FILE_ANSI 模式时才有效。如果字符串以 Unicode (FILE_UNICODE) 存储,则代码页无关紧要。
如果成功,函数返回文件描述符(一个正整数)。它仅在特定 MQL 程序内唯一;没有必要与其它程序共享。为进一步处理文件,描述符被传递到对其它函数的调用。
如果出错,结果为 INVALID_HANDLE (-1)。错误本质应通过 GetLastError 函数返回的代码来阐明。
文件打开时进行的所有操作模式设置在文件保持打开期间保持不变。如果需要更改模式,应关闭文件再以新的参数重新打开。
对于每个打开的文件,MQL 程序执行环境保持一个内部指针,也就是在文件中的当前位置。文件打开之后,指针立即设置到开头(位置 0)。在读写过程中,该位置根据从各种文件函数传递或接收到的数据量相应进行偏移。也可以直接影响位置(前移或后移)。所有这些可能情况将在后续章节讨论。
FILE_READ 和 FILE_WRITE 的不同组合可让你实现若干场景:
- FILE_READ 仅当文件存在时才打开文件;否则,该函数返回错误,不会创建新文件。
- FILE_WRITE 如果文件不存在,则创建新文件;或者打开现有文件,清除其内容,大小被重置为零。
- FILE_READ|FILE_WRITE 打开现有文件并保留全部内容,或者如果文件不存在,则创建新文件。
可以看到,某些场景无法实现完全是因为标志的原因。尤其是如果文件尚不存在,则不能打开文件进行写入操作。这可以使用其它函数实现,例如, FileIsExist。此外,无法“自动”重置一个打开用于读写组合操作的文件:在此情况下,MQL5 始终保留内容。
要将数据附加到文件,必须不仅以 FILE_READ|FILE_WRITE 模式打开该文件,而且还要通过调用 FileSeek将文件中的当前位置移动到其末尾。
正确描述文件的共享访问是成功执行 File Open 的前提条件。这方面的管理规则如下:
- 若未指定 FILE_SHARE_READ 和 FILE_SHARE_WRITE 标志,如果当前程序首先打开该文件,则获得对该文件的独占访问。如果该文件之前已被打开(被其它程序或同一程序),则函数调用将会失败。
- 若设置了 FILE_SHARE_READ 标志,程序允许后续请求打开该相同文件进行读取。如果在函数调用时该文件已被其它程序或同一程序打开进行读取,并且未设置该标志,则该函数调用将会失败。
- 若设置了 FILE_SHARE_WRITE 标志,程序允许后续请求打开该相同文件进行写入。如果在函数调用时该文件已被其它程序或同一程序打开进行写入,并且未设置该标志,则该函数调用将会失败。
访问共享检查不仅相对于其它 MQL 程序或 MetaTrader 5 以外的外部进程,而且相对于同一 MQL 程序(如果它重新打开该文件)。
因此,冲突最少的模式意味着同时指定两个标志,但这仍然不能保证在某一方获得对该文件的描述符且未共享的情况下,可以打开该文件。然而,根据计划的读写,应遵循更严格的规则。
例如,当打开文件进行读取时,允许其他程序读取该文件是合理的。此外,如果某个文件是追加补充型(例如日志),你可能允许其他程序对该文件写入。但是,当打开文件进行写入时,便没有必要允许其他程序进行写入了,否则会导致不可预测的数据重叠。
void FileClose(int handle)
该函数通过句柄关闭先前打开的文件。
该文件关闭后,其在程序中的句柄变为无效:试图对该文件调用任何文件函数将导致错误。但是,如果你重新打开同一文件或不同文件,你可以使用同一变量存储不同句柄。
当程序终止,打开的文件强制关闭,如果写缓冲区不为空,则被写入磁盘。但是建议显式关闭文件。
务必遵循以下规则:完成文件的处理后关闭文件。这不仅因为未关闭的文件可能导致写入的信息因被缓存而在 RAM 中保留一段时间且不会保存到磁盘(如前所述)。此外,打开的文件会消耗操作系统的内部资源,不是消耗磁盘空间。同时打开的文件的数量受到限制(可能是数百个或数千个,取决于 Windows 设置)。如果很多程序让大量文件处于打开状态,可能达到该限值,导致尝试打开新文件失败。
对此,最好使用一个具有以下功能的包装器类来防止可能的描述符丢失:该包装器类会打开文件并在创建对象时接收到一个描述符,在析构函数中释放描述符,文件自动关闭。
我们将在测试纯 FileOpen 和 FileClose 函数后创建一个包装器类。
但在深入了解文件细节之前,我们准备一个新版本的宏,用以说明我们的函数在调用日志中的输出。之所以需要使用新版本的宏,是因为截至目前,诸如 PRT 和 PRTS 的宏(在前面章节中使用)在打印期间会“吸收”函数返回值。例如,我们编写:
PRT(FileLoad(filename, read)); |
其中 FileLoad 调用的结果被发送到日志,但无法在调用代码字符串中获取它。坦白讲,我们之前并不需要它。但是现在 FileOpen 函数将返回一个文件描述符,并且应被存储在一个变量中,用于该文件的进一步操作。
之前的宏有两个问题。首先,它们基于函数 Print,该函数消耗传递的数据(将其发送到日志),但不返回任何内容。其次,有结果的变量的任何值只能从表达式获得,Print 调用不能成为表达式的一部分,因为它的类型为 void。
为解决这些问题,我们需要打印能够返回可打印值的辅助函数。我们将其调用包装进一个新的 PRTF 宏:
#include <MQL5Book/MqlError.mqh>
|
使用 '#’ 魔法字符串转换运算符,我们获得被作为第一自变量传递到 ResultPrint 的代码段(表达式 A)的详细描述符。该表达式本身(宏自变量)会被求值(如果有函数,该函数被调用),其结果被作为第二自变量传递给 ResultPrint。接下来,常规 Print 函数就派上用场了,最后,将相同结果返回至调用代码。
为避免查阅帮助文档解析错误代码,准备了 E2S 宏,该宏使用包含所有 MQL5 错误的 MQL_ERROR 枚举。可以在头文件 MQL5/Include/MQL5Book/MqlError.mqh 中找到这个宏。新宏和 ResultPrint 函数在 PRTF.mqh 文件中定义,与测试脚本同属一个级别。
在 FileOpenClose.mq5 脚本中,我们尝试打开不同的文件,尤其是同一文件将并行打开若干次。这在真实程序中通常可以避免。对于程序实例中的特定文件,单个句柄即可满足大多数任务的需求。
其中一个文件 MQL5Book/rawdata 必须已经存在,因为它由章节 简化模式下的文件读写中的脚本创建。另一个文件将在测试期间创建。
我们将选择文件类型 FILE_BIN。在这一阶段,处理 FILE_TXT 或 FILE_CSV 相似。
我们为文件描述符预留一个数组,以便在脚本结束时一次性关闭所有文件。
首先,我们以读模式打开 MQL5Book/rawdata,禁用访问共享。假设该文件未被任何第三方应用程序使用,则我们预期可成功接收到句柄。
void OnStart()
|
如果我们尝试再次打开同一文件,将会碰到错误,因为第一或第二个调用均不允许共享。
ha[1] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ)); // -1 / CANNOT_OPEN_FILE(5004) |
我们关闭第一个句柄,再次打开该文件,但是以共享读权限打开,确保现在可以再次打开(尽管仍然需要允许共享读取):
FileClose(ha[0]);
|
打开文件进行写入 (FILE_WRITE) 则不行,因为对 FileOpen 的先前两次调用仅允许 FILE_SHARE_READ。
ha[2] = PRTF(FileOpen(rawdata, FILE_BIN | FILE_READ | FILE_WRITE | FILE_SHARE_READ));
|
现在我们尝试创建一个新文件 MQL5Book/newdata。如果将其以只读模式打开,则不会创建文件。
const string newdata = "MQL5Book/newdata";
|
要创建文件,必须指定 FILE_WRITE 模式(在这里,是否包含 FILE_READ 并不重要,但包含的话可以提高调用的通用性:我们应该还记得,在该组合中,指令保证旧文件如果存在则将被打开,或者将创建新文件)。
ha[3] = PRTF(FileOpen(newdata, FILE_BIN | FILE_READ | FILE_WRITE)); // 3 / ok |
我们尝试使用我们已经知道的函数 FileSave 将某些内容写入到新文件。它扮演“外部玩家”的角色,因为它绕过我们的描述符来处理文件,方式大致与其它 MQL 程序或第三方应用程序的处理方式相同。
long x[1] = {0x123456789ABCDEF0};
|
该调用失败,因为在没有共享权限的情况下打开了句柄。关闭该文件并以最大“权限”重新打开。
FileClose(ha[3]);
|
这次 FileSave 如预期的那样工作。
PRTF(FileSave(newdata, x)); // true |
你可以在文件夹 MQL5/Files/MQL5Book/ 中找到 newdata 文件,长度为 8 个字节。
请注意,我们关闭了该文件之后,其描述符返回到空闲描述符池中,下次打开一个文件(或是另一个文件)时,相同编号再次出现。
为了让文件正常关闭,我们将显式关闭所有打开的文件。
for(int i = 0; i < ArraySize(ha); ++i)
|