读写数组
两个 MQL5 函数设计用于读写数组:FileWriteArray 和 FileReadArray。利用二进制文件,可让你处理除字符串以外任何内置类型的数组,以及不包含字符串字段、对象、指针和动态数组的简单结构体数组。这些限制与读写过程的优化相关,由于排除了可变长度类型,这种优化才得以实现。字符串、对象和动态数组就是可变长度类型的数组。
同时,在处理文本文件时,这些函数能够操作 string 类型的数组(这些函数不允许具有 FILE_TXT/FILE_CSV 模式的文件中其它类型的数组)。这些数组使用以下格式存储在文件中:每行一个元素。
如果你需要没有类型限制地在文件中存储结构体或类,请使用类型特定函数(每次调用处理一个值)。这些函数在有关读写内置类型变量的两个章节中描述:分别针对 二进制 和 文本 文件。
此外,对于带字符串结构体的支持可通过信息存储的内部优化来实现。例如,相比使用字符串字段,你可以改为使用整数字段,此类字段将在包含带字符串的单独数组中存储对应字符串的索引。鉴于使用 OOP 工具重新定义多种运算(尤其是赋值运算)以及按编号获取数组的结构元素的可能性,算法的外观形式将基本不变。但在编写时,你可以首先以二进制模式打开一个文件,然后为具有简化结构类型体的数组调用 FileWriteArray,然后以文本模式重新打开该文件,并再次 FileWriteArray 将一个所有字符串的数组添加到该文件。要读取这样一个文件,你应在其开头提供一个包含数组中元素数量的标头,以便将其作为 count 参数传递到 FileReadArray 中(参见下文)。
如果你需要保存或读取的不是一个结构体数组,而是单个结构,则使用 FileWriteStruct 和 FileReadStruct 函数,这两个函数在 下一章节中描述。
我们来了解函数签名,然后看一个普通示例 (FileArray.mq5)。
uint FileWriteArray(int handle, const void &array[], int start = 0, int count = WHOLE_ARRAY)
该函数将 array array 写入到一个具有 handle 描述符的文件。该数组可以是多维数组。start 和 count 参数允许设置元素范围;默认等于整个数组。对于多维数组,start 索引和元素数量 count 指跨所有维度(而不只是数组的第一维度)的连续编号。例如,如果数组具有配置 [][5], 则等于 7 的 start 值将指向索引为 [1][2] 的元素,并且 count = 2 将把元素 [1][3] 添加到数组。
该函数返回写入元素的数量。如果出错,则将返回 0。
如果以二进制模式接收到 handle,则数组可以是除字符串以外的任何内置类型,或者简单结构体类型。如果 handle 以任何文本模式打开,则数组必须是 string 类型。
uint FileReadArray(int handle, const void &array[], int start = 0, int count = WHOLE_ARRAY)
该函数将数据从一个具有 handle 描述符的文件读入到一个数组。该数组可以是多维数组和动态数组。对于多维数组,start 和 count 参数基于如上所述的所有维度中元素的连续编号生效。动态数组在必要时自动增加大小以适配要读取的数据。如果 start 大于数组的原始长度,则这些中间元素将在内存分配后包含随机数据(参见示例)。
请注意,该函数不能控制写文件时使用的数组配置与读取时接收数组配置的一致性。一般来说,无法保证被读取的文件是以 FileWriteArray 写入。
为检查数据结构的有效性,通常使用初始标头的预定义格式或文件中的其它描述符。这些函数将读取在文件大小内的任何文件内容,并将其放入指定数组中。
如果以二进制模式接收到 handle,则数组可以是任何内置非字符串类型或简单结构体类型。如果 handle 以文本模式打开,则数组必须是 string 类型。
我们使用 FileArray.mq5 脚本检查二进制模式和文本模式工作情况。为此,我们将保留两个文件名。
const string raw = "MQL5Book/array.raw";
|
OnStart 函数中描述了三个 long 类型的数组和两个 string 类型的数组。仅每种类型的第一个数组被填充数据,所有其它数组将在文件被写入后检查读取情况。
void OnStart()
|
此外,为测试结构体操作,定义了以下 3 个类型:
struct TT
|
我们将不能够在描述的函数中使用 TT 类型的结构体,因为它包含字符串字段。需要展示注释语句中潜在的编译错误(参见下文)。B 和 XYZ 结构体之间的继承关系以及是否存在封闭字段并不影响 FileWriteArray 和 FileReadArray 函数。
结构体用于声明一对数组:
TTtt[]; // empty, because data is not important
|
我们从二进制模式开始。我们创建一个新文件或者打开一个现有文件,转储其内容。然后,在三次 FileWriteArray 调用中,我们将尝试写入三个数组:numbers1、text1 以及 xyz。
int writer = PRTF(FileOpen(raw, FILE_BIN | FILE_WRITE)); // 1 / ok
|
numbers1 和 xyz 数组成功写入,写入的项目数表明了这一点。text1 数组失败,错误代码 FILE_NOTTXT(5012),因为字符串数组要求文件以文本模式打开。因此,内容 xyz 将位于文件中紧跟在 numbers1 的所有元素之后。
注意每个读/写函数开始在文件中的当前位置开始读/写数据,并以读/写数据的大小偏移位置。如果在写操作之前指针位于文件末尾,则文件大小增加。如果在读取过程中到达文件末尾,则指针不再移动,系统发出特殊内部错误代码 5027 (FILE_ENDOFFILE)。在零大小的新文件中,开头和末尾相同。
从数组 text1 写入了 0 项,因此文件中无任何内容提示你在两次成功的 FileWriteArray 调用之间有一次失败。
在测试脚本中,我们只是将函数结果和状态(错误代码)输出到日志,但在真实程序中,我们应即时分析问题并采取某些措施:修正参数中的错误以及文件设置中的问题,或者中断进程并向用户发送消息。
我们将文件读入 numbers2 数组。
int reader = PRTF(FileOpen(raw, FILE_BIN | FILE_READ)); // 1 / ok
|
由于不同的数组被写入文件(不仅是 numbers1,还有 xyz),8 个元素被读入接收数组中(即从头至尾整个文件,因为未使用参数指定范围)。
实际上,结构体 XYZ 的大小是 16 个字节(4 个 4 字节的字段):一个 int 和三个 color),其对应于 numbers2 数组中的一行(2 个long 类型的元素)。在这里只是巧合。因为如上面所提到的,函数并不清楚原始数据的配置和大小,可能将任何内容读入任何数组:编程人员必须监控操作的有效性。
我们比较初始状态和接收到的状态。源数组 numbers1:
[,0][,1]
|
生成数组 numbers2:
[,0] [,1]
|
numbers2 数组的开头与原始 numbers1 数组完全匹配,即通过文件进行的读写操作正常。
最后一行完全由单个结构体 XYZ 占用(值正确,但表示方式不正确,表示为两个 long 类型的数字)。
现在我们转到文件开头(使用 FileSeek 函数,我们将稍后在 文件内的位置控制章节中讨论该函数),并调用 FileReadArray 指示元素的编号和数量,即我们执行部分读取。
PRTF(FileSeek(reader, 0, SEEK_SET)); // true
|
从该文件读取了三个元素,并从索引 10 开始读入接收数组 numbers3。由于该文件是从开头读取,因此这些元素是值 1、4、2。由于二维数组具有配置 [][2],因此穿透索引 10 指向元素 [5,0]。内存中的情况是这样的:
[,0][,1]
|
标为黄色的项是随机产生的(对于不同的脚本运行可能有所变化)。有可能它们全部为零,但不能保证。numbers3 数组最初为空,FileReadArray 调用发起内存分配,且分配以偏移 10 接收 3 个元素(共 13)所需的内存量。所选块未填空任何内容,仅从该文件读取 3 个数字。因此,具有穿透索引 0 至 9 的元素(即前 5 行)以及最后一个元素(索引 13)包含无用信息。
多维数组沿第一维缩放,因此,增加 1 个数字表示沿更高维度添加整个配置。在此情况下,分布涉及一个由两个数字组成的系列 ([][2])。换言之,要求的大小 13 被舍入为二的倍数,亦即 14。
最后,我们测试函数处理字符串数组的方式。我们创建一个新文件或者打开一个现有文件,转储其内容。然后,在两次 FileWriteArray 调用中,我们将写入 text1 和 numbers1 数组。
writer = PRTF(FileOpen(txt, FILE_TXT | FILE_ANSI | FILE_WRITE)); // 1 / ok
|
字符串数组成功保存。数值数组被忽略并提示 FILE_NOTBIN(5011) 错误,因为必须以二进制模式打开文件。
当尝试写入一个结构体数组 tt 时,会出现编译错误,并显示长消息“不允许使用带对象的结构体或类”。编译器的实际意思是,它不接受像 string 这样的字段(假定字符串和动态数组具有某些服务对象的内部表示)。因此,尽管文件是以文本模式打开并且在结构体中仅有文本字段,这一组合在 MQL5 中也不被支持。
// COMPILATION ERROR: structures or classes containing objects are not allowed
|
字符串字段的存在使得结构体变得复杂,不适合以任何模式处理 FileWriteArray/FileReadArray 函数。
运行该脚本之后,你可以切换到 MQL5/Files/MQL5Book 目录,检查生成文件的内容。
早前在 简化模式下的文件读写章节中,我们讨论了 FileSave 和 FileLoad 函数。在测试脚本 (FileSaveLoad.mq5) 中,我们使用 FileWriteArray 和 FileReadArray 实现了这些函数的等效版本。但我们尚未详细了解它们。现在我们熟悉了这些新函数,我们可以检查源代码:
template<typename T>
|
MyFileSave 基于对 FileWriteArray 的单次调用实现,而 MyFileLoad 则基于对 FileReadArray 调用而实现,两者均在一对 FileOpen/FileClose 调用之间。在两种情况下,所有可用数据都会被读写。凭借模板,我们的函数能够接受任意类型的数组。但是如果任何不支持的类型(例如类)被推导为元参数 T,则将会发生编译错误,与对内置函数的不正确访问时的情况一样。