管理在文件中的位置
我们已知知道,系统将某个指针与每个打开的文件关联:该指针确定下次任何 I/O 函数被调用时在文件中开始写入或读取数据的位置(相对于开头位置的偏移)。函数执行后,指针将偏移写入或读取数据的大小。
在某些情况下,你需要在没有 I/O 操作的情况下更改指针的位置。尤其是我们需要将数据附加到文件末尾时,我们以“混合”模式 FILE_READ | FILE_WRITE 打开文件,此时必须想办法将指针定位在文件末尾(否则我们将从开头开始覆写数据)。虽然我们可以在有内容需要读取的情况下调用读函数(从而偏移指针),但这种方式的效率不高。最好使用专用函数 FileSeek。FileTell 函数允许获取指针的实际值(在文件中的位置)。
在本节中,我们将探讨这两个函数以及与文件中当前位置相关的一些其它函数。有些函数对于文本和二进制模式的文件工作方式相同,而其它一些函数则不同。
bool FileSeek(int handle, long offset, ENUM_FILE_POSITION origin)
该函数将文件指针移动 offset 字节数(使用 origin 为参考点,该参考点是 ENUM_FILE_POSITION 枚举中描述的预定义位置之一)。offset 可以是正值(移动到文件末尾及以外)或负值(移动到开头)。ENUM_FILE_POSITION 具有以下成员:
- SEEK_SET 表示文件开头
- SEEK_CUR 表示当前位置
- SEEK_END 表示文件末尾
如果相对于锚点的新位置计算结果为负值(即请求了向文件开头左边的偏移),则文件指针将被设置为文件开头。
如果你将位置设为超出文件末尾(值大于文件大小),则随后对文件的写入将不是从文件末尾进行,而是从设置的位置进行。在此情况下,未定义的值将被写入到之前的文件末尾与给定位置之间(见下文)。
如果成功,函数返回 true,如果出错,返回 false 。
ulong FileTell(int handle)
对于使用 handle 描述符打开的文件,该函数返回内部指针的当前位置(相对于文件开头的偏移)。如果出错,将返回 ULONG_MAX ((ulong)-1)。该错误代码在 _LastError 变量中提供,也可以通过 GetLastError 函数获取。
bool FileIsEnding(int handle)
该函数返回关于指针是否在 handle 文件末尾的指示。如果是,则结果为 true。
bool FileIsLineEnding(int handle)
对于具有 handle 描述符的文件,该函数返回一个指示文件指针是否在文件末尾的符号(紧接在换行符字符 '\n' 或 '\r\n' 之后)。换言之,返回值 true 表示当前位置在下一行的开头(或者在文件末尾)。对于二进制文件,结果始终为 false。
前述函数的测试脚本名为 FileCursor.mq5。它处理三个文件:两个二进制文件和一个文本文件。
const string fileraw = "MQL5Book/cursor.raw";
const string filetxt = "MQL5Book/cursor.csv";
const string file100 = "MQL5Book/k100.raw";
|
为简化对当前位置的记录,除了文件末尾 (End-Of-File, EOF) 以及行末 (End-Of-Line, EOL) 符号,我们还创建了辅助函数 FileState。
string FileState(int handle)
{
return StringFormat("P:%I64d, F:%s, L:%s",
FileTell(handle),
(string)FileIsEnding(handle),
(string)FileIsLineEnding(handle));
}
|
在二进制文件上测试这种函数的场景包括以下步骤。
以读/写模式新建或打开一个现有 fileraw 文件 ("MQL5Book/cursor.raw")。在打开之后以及在每个操作之后,立即通过调用 FileState 输出文件的当前状态。
void OnStart()
{
int handle;
Print("\n * Phase I. Binary file");
handle = PRTF(FileOpen(fileraw, FILE_BIN | FILE_WRITE | FILE_READ));
Print(FileState(handle));
...
|
将指针移动到文件末尾,这将允许我们在每次执行脚本时将数据附加到该文件(而不是从开头覆写文件)。最明显的表示文件末尾的方式:相对于 origin=SEEK_END 的零 offset。
PRTF(FileSeek(handle, 0, SEEK_END));
Print(FileState(handle));
|
如果文件不再为空(非新),则我们可以在其任意位置(相对或绝对位置)读取现有数据。尤其是,若 FileSeek 函数的 origin 参数等于 SEEK_CUR,这表明如果 offset 为负,则当前位置将往后(向左)移动对应的字节数,如果为正,则往前移动(向右)。
在本示例中,我们尝试以 int 类型值的大小回退。稍后我们将会看到,在该位置应该有一个以下结构体的 day_of_year 字段(最后字段): MqlDateTime,因为我们在后续指令中将其写入到一个文件,在下次运行时可从文件中读取该数据。读取的值被记录,与之前保存的值进行比较。
if(PRTF(FileSeek(handle, -1 * sizeof(int), SEEK_CUR)))
{
Print(FileState(handle));
PRTF(FileReadInteger(handle));
}
|
在一个新的空文件中,FileSeek 调用将以错误 4003 (INVALID_PARAMETER) 结束,而 if 语句块将不被执行。
接下来,以数据填充该文件。首先,使用 FileWriteLong 写入计算机的当前本地时间(8 字节的 datetime)。
datetime now = TimeLocal();
PRTF(FileWriteLong(handle, now));
Print(FileState(handle));
|
然后我们尝试从当前位置回退 4 个字节 (-4) 并读取 long。
PRTF(FileSeek(handle, -4, SEEK_CUR));
long x = PRTF(FileReadLong(handle));
Print(FileState(handle));
|
这一尝试将出现错误 5015 (FILE_READERROR),因为此时处于文件末尾,向左偏移 4 个字节后无法从右侧读取 8 个字节(long 大小)。但是,如同我们将会从日志所看到的那样,虽然这一尝试不成功,但指针仍将回移到文件末尾。
如果回退 8 个字节 (-8),则后续对 long 值的读取将会成功,并且两个时间值(包括原始值和从文件接收到的值)必须匹配。
PRTF(FileSeek(handle, -8, SEEK_CUR));
Print(FileState(handle));
x = PRTF(FileReadLong(handle));
PRTF((now == x));
|
最后,将填充了相同时间的 MqlDateTime 结构体写入到文件。文件中的位置将增加 32(以字节表示的结构体大小)。
MqlDateTime mdt;
TimeToStruct(now, mdt);
StructPrint(mdt); // display the date/time in the log visually
PRTF(FileWriteStruct(handle, mdt)); // 32 = sizeof(MqlDateTime)
Print(FileState(handle));
FileClose(handle);
|
在针对使用 fileraw 文件 (MQL5Book/cursor.raw) 的场景第一次脚本运行之后,我们得到如下内容(时间将不相同):
first run
* Phase I. Binary file
FileOpen(fileraw,FILE_BIN|FILE_WRITE|FILE_READ)=1 / ok
P:0, F:true, L:false
FileSeek(handle,0,SEEK_END)=true / ok
P:0, F:true, L:false
FileSeek(handle,-1*sizeof(int),SEEK_CUR)=false / INVALID_PARAMETER(4003)
FileWriteLong(handle,now)=8 / ok
P:8, F:true, L:false
FileSeek(handle,-4,SEEK_CUR)=true / ok
FileReadLong(handle)=0 / FILE_READERROR(5015)
P:8, F:true, L:false
FileSeek(handle,-8,SEEK_CUR)=true / ok
P:0, F:false, L:false
FileReadLong(handle)=1629683392 / ok
(now==x)=true / ok
2021 8 23 1 49 52 1 234
FileWriteStruct(handle,mdt)=32 / ok
P:40, F:true, L:false
|
根据状态,文件大小初始为零,因为偏移到文件末尾 ("F:true") 之后位置为 "P:0"。每次记录之后(使用 FileWriteLong 和 FileWriteStruct),位置 P 将增大写入数据的大小。
在第二次脚本运行之后,你会注意到日志中的某些变化:
second run
* Phase I. Binary file
FileOpen(fileraw,FILE_BIN|FILE_WRITE|FILE_READ)=1 / ok
P:0, F:false, L:false
FileSeek(handle,0,SEEK_END)=true / ok
P:40, F:true, L:false
FileSeek(handle,-1*sizeof(int),SEEK_CUR)=true / ok
P:36, F:false, L:false
FileReadInteger(handle)=234 / ok
FileWriteLong(handle,now)=8 / ok
P:48, F:true, L:false
FileSeek(handle,-4,SEEK_CUR)=true / ok
FileReadLong(handle)=0 / FILE_READERROR(5015)
P:48, F:true, L:false
FileSeek(handle,-8,SEEK_CUR)=true / ok
P:40, F:false, L:false
FileReadLong(handle)=1629683397 / ok
(now==x)=true / ok
2021 8 23 1 49 57 1 234
FileWriteStruct(handle,mdt)=32 / ok
P:80, F:true, L:false
|
首先,打开后的文件大小为 40 (依据是偏移到文件末尾后的位置 "P:40")。每次脚本运行后,文件大小将增加 40 字节。
其次,由于文件不为空,无法在其中导航并读取“旧”数据。尤其是从当前位置(同时也是文件末尾)回退到 -1*sizeof(int) 之后,我们成功读取了值 234,这是结构体 MqlDateTime 的最后字段(它是一年中“日”,对于你来说,很可能不同)。
第二个测试场景是处理文本 CSV 文件 filetxt (MQL5Book/cursor.csv)。我们同样将以读写组合模式打开它,但不将指针移动到文件末尾。因此,每次脚本运行将从文件开头开始覆写数据。为方便看到差异,CSV 文件第一列中的数字是随机生成的。在第二列中,相同字符串始终替换为 StringFormat 函数中的模板。
Print(" * Phase II. Text file");
srand(GetTickCount());
// create a new file or open an existing file for writing/overwriting
// from the very beginning and subsequent reading; inside CSV data (Unicode)
handle = PRTF(FileOpen(filetxt, FILE_CSV | FILE_WRITE | FILE_READ, ','));
// three rows of data (number,string pair in each), separated by '\n'
// note that the last element does not end with a newline '\n'
// this is optional, but allowed
string content = StringFormat(
"%02d,abc\n%02d,def\n%02d,ghi",
rand() % 100, rand() % 100, rand() % 100);
// '\n' will be replaced with '\r\n' automatically, thanks to FileWriteString
PRTF(FileWriteString(handle, content));
|
下面是生成数据的示例:
然后我们将回到文件开头,并使用 FileReadString 在循环中读取数据,持续记录状态。
PRTF(FileSeek(handle, 0, SEEK_SET));
Print(FileState(handle));
// count the lines in the file using the FileIsLineEnding feature
int lineCount = 0;
while(!FileIsEnding(handle))
{
PRTF(FileReadString(handle));
Print(FileState(handle));
// FileIsLineEnding also equals true when FileIsEnding equals true,
// even if there is no trailing '\n' character
if(FileIsLineEnding(handle)) lineCount++;
}
FileClose(handle);
PRTF(lineCount);
|
下面是第一和第二次脚本运行后 filetxt 文件的日志。首先是第一个:
first run
* Phase II. Text file
FileOpen(filetxt,FILE_CSV|FILE_WRITE|FILE_READ,',')=1 / ok
FileWriteString(handle,content)=44 / ok
FileSeek(handle,0,SEEK_SET)=true / ok
P:0, F:false, L:false
FileReadString(handle)=08 / ok
P:8, F:false, L:false
FileReadString(handle)=abc / ok
P:18, F:false, L:true
FileReadString(handle)=37 / ok
P:24, F:false, L:false
FileReadString(handle)=def / ok
P:34, F:false, L:true
FileReadString(handle)=96 / ok
P:40, F:false, L:false
FileReadString(handle)=ghi / ok
P:46, F:true, L:true
lineCount=3 / ok
|
下面是第二个:
second run
* Phase II. Text file
FileOpen(filetxt,FILE_CSV|FILE_WRITE|FILE_READ,',')=1 / ok
FileWriteString(handle,content)=44 / ok
FileSeek(handle,0,SEEK_SET)=true / ok
P:0, F:false, L:false
FileReadString(handle)=34 / ok
P:8, F:false, L:false
FileReadString(handle)=abc / ok
P:18, F:false, L:true
FileReadString(handle)=20 / ok
P:24, F:false, L:false
FileReadString(handle)=def / ok
P:34, F:false, L:true
FileReadString(handle)=02 / ok
P:40, F:false, L:false
FileReadString(handle)=ghi / ok
P:46, F:true, L:true
lineCount=3 / ok
|
如你所见,文件大小不变,但以相同偏移写入了不同的数字。由于该 CSV 文件有两列,在每个我们读取的第二个值之后,我们都会看到有一个 EOL 标记 ("L:true")。
检测到的行数为 3,尽管文件中仅有 2 个换行符:最后一行(第三行)结束文件。
最后一个测试场景使用文件 file100 (MQL5Book/k100.raw) 将指针移动超过文件末尾(至 1000000 字节标记),从而增加的文件大小(为可能的将来写操作预留磁盘空间)。
Print(" * Phase III. Allocate large file");
handle = PRTF(FileOpen(file100, FILE_BIN | FILE_WRITE));
PRTF(FileSeek(handle, 1000000, SEEK_END));
// to change the size, you need to write at least something
PRTF(FileWriteInteger(handle, 0xFF, 1));
PRTF(FileTell(handle));
FileClose(handle);
|
该脚本的日志输出对于每次运行都相同,只是最终位于为文件分配的空间中的随机数据可能不同(其内容在这里未显示:请使用外部二进制文件查看器)。
* Phase III. Allocate large file
FileOpen(file100,FILE_BIN|FILE_WRITE)=1 / ok
FileSeek(handle,1000000,SEEK_END)=true / ok
FileWriteInteger(handle,0xFF,1)=1 / ok
FileTell(handle)=1000001 / ok
|