读写变量(二进制文件)
如果一个结构体包含简单结构体所禁止的字段类型(字符串、动态数组、指针),使用前面介绍的函数将无法将其写入到文件或从文件读取。类对象也是如此。然而,此类实体通常包含程序中的大部分数据,并且也要求保存和还原它们的状态。
使用前一节中的标头结构体示例,很明显,可以避免字符串(以及其它可变长度类型),但在此情况下,就不得不创造替代性的更繁琐的算法实施(例如,以字符数组替换字符串)。
为读写任意复杂度的数据,MQL5 提供了一套低层级函数,可对特定类型的单个值进行操作:double、float、int/uint、long/ulong 或者 string。所有其它内置 MQL5 类型均等效于不同大小的整数:char/uchar 是 1 字节,short/ushort 是 2 字节,color 是 4 字节,枚举是 4 字节,datetime 是 8 字节。这些函数可以称为原子函数(即不可见),因为用于在位层级读写文件的函数已不再存在。
当然,逐元素读写也可消除动态数组文件操作限制。
至于对象指针,根据 OOP 范式精神,我们可以允许它们保存和还原对象:只需在每个类中实现一个接口(一系列方法),该接口负责使用低层级函数负责向文件传输和回传重要内容。然后,如果对象中包含指向其他对象的指针字段,我们直接将保存或读取任务委托给该对象,而该对象将处理自己的字段(其中可能有其它指针),该委派将逐层递进,直至涵盖所有元素。
请注意,在本节中,我们将探讨二进制文件的原子函数。文本文件的原子函数将在 下一章节中介绍。本节中所有的函数均返回写入的字节数,如果出错,则返回 0。
uint FileWriteDouble(int handle, double value)
uint FileWriteFloat(int handle, float value)
uint FileWriteLong(int handle, long value)
这些函数将 value (double, float, long) 参数中传递的对应类型的值写入到一个具有 handle 描述符的二进制文件。
uint FileWriteInteger(int handle, int value, int size = INT_VALUE)
该函数将 value 整数写入到一个具有 handle 描述符的二进制文件。值的大小(以字节为单位)由 size 参数设置,并且可以是预定义的常量之一:CHAR_VALUE (1)、SHORT_VALUE (2)、INT_VALUE(默认 4),其对应于 char、short 以及 int 类型(有符号和无符号)。
该函数支持 3 字节整数的未正式公开的写入模式。不建议使用。
文件指针以写入的字节数移动(而不是以 int 大小)。
uint FileWriteString(int handle, const string value, int length = -1)
该函数将来自 value 参数的字符写入到一个具有 handle 描述符的二进制文件。你可以指定用于写入 length 参数的字符数。如果它小于字符串长度,则仅指定的字符串部分将被包括在文件中。如果 length 为 -1 或未指定,则整个字符串被传输到没有终止符 null 的文件。如果 length 大于字符串的长度,则额外字符以零填充。
请注意,当写入到一个以 FILE_UNICODE 标志(或者没有使用 FILE_ANSI 标志)打开的文件时,字符串以 Unicode 格式保存(每个字符占 2 个字节)。当写入到一个以 FILE_ANSI 标志打开的文件时,每个字符占用 1 个字节(外语字符可能失真)。
FileWriteString 函数也能够处理文本文件。这方面的应用在下一节中描述。
double FileReadDouble(int handle)
float FileReadFloat(int handle)
long FileReadLong(int handle)
这些函数可从具有指定描述符的二进制文件读取适当类型的数字(double、float 或 long)。如果必要,将结果转换为 ulong(如果在文件中该位置需要无符号长整型数字)。
int FileReadInteger(int handle, int size = INT_VALUE)
该函数从一个具有 handle 描述符的二进制文件读取整数值。值大小(以字节数表示)在 size 参数中指定。
由于该函数的结果是 int 类型,因此,如果要求的目标类型不是 int,则必须显式转换为要求的目标类型(即转换为 uint 或 short/ushort 或 char/uchar)。否则,将至少会出现编译器警告,甚至丢失符号。
事实是,在读取 CHAR_VALUE 或 SHORT_VALUE 时,默认结果始终为正(即对应于 uchar 和 ushort,其完全适配 int)。在这些情况下,如果数字实际上是 uchar 和 ushort 类型,则编译器警完全是名义上的,因为我们已经确定,在 int 类型的值中,仅填充 1 或 2 个低字节,并且它们是无符号的。这并不会产生失真。
然而,在将无符号值(char 和 short 类型)存储到文件中时,有必要进行转换,因为如果不转换,则负值将变为具有相同位表示的逆序正值(参见 算术类型转换 一节中的“无符号和有符号整数”部分)。
任何情况下,最好进行显式类型转换以避免出现警告。
该函数支持 3 字节整数读取模式。不建议使用。
文件指针以写入的字节数移动(而不是以 int 大小)。
string FileReadString(int handle, int size = -1)
该函数从一个具有 handle 描述符的文件读取一个具有以字符数表示的指定大小的字符串。处理二进制文件时,必须设置 size 参数(默认值仅适合于使用分隔符字符的文本文件)。否则,不能读取字符串(函数返回空字符串),并且内部错误代码 _LastError 为 5016 (FILE_BINSTRINGSIZE)。
因此,即使在将字符串写入二进制文件的阶段,也需要考虑字符串的读取方式。有三个主要选项:
- 写入末尾带空终止符的字符串。在此情况下,必须在循环中逐字符分析它们,并将字符组合为一个字符串,直至遇到 0 为止。
- 始终写入固定(预定义)长度的字符串。对于大多数场景,长度应带余量选择,或者根据规范(参考范围、协议等)选择,但这并不经济,并且不能百分百保证在写入到文件时,某些罕见字符串不会被缩短。
- 在字符串前面写入长度值(整数)。
FileReadString 函数也能够处理文本文件。这方面的应用在下一节中描述。
另外,请注意,如果 size 参数为 0(在某些计算中可能发生),则该函数不执行读取操作:文件指针保持在相同位置,函数返回一个空字符串。
作为本节的一个示例,我们将改进上一节中的 FileStruct.mq5 脚本。新的程序名称为 FileAtomic.mq5。
任务不变:将一个给定的带有报价的截断 MqlRates 结构体的数字保存到一个二进制文件。但现在 FileHeader 结构体将变为一个类(并且格式签名将存储在一个字符串中,而不是存储在字符数组中)。一个该类型的标头和一个报价数组将成为另一个控制类 Candles 的一部分,并且两个类都将继承自 Persistent 接口,用于将任意对象写入到一个文件以及从一个文件读取任意对象。
接口如下:
interface Persistent
|
在 FileHeader 类中,我们将实现对格式签名(我们将其更改为 "CANDLES/1.1")以及当前交易品种名称和图表时间范围的保存和检查(有关 _Symbol 和 _Period的更多信息)。
写入是在对继承自接口的 write 方法的实现中完成。
class FileHeader : public Persistent
|
签名准确根据其长度写入,因为样本存储在对象中,在读取时将设置相同的长度。
对于当前图表中的金融工具,我们首先将其名称长度保存在文件中(对于最大 255 的长度,1 个字节即足够),然后我们才保存字符串本身。
如果排除常量前缀 "PERIOD_",时间范围的名称绝不会超过 3 个符号,因此,为该字符串选择一个固定长度。不带前缀的时间范围名称在辅助函数 PeriodToString 中获得:该函数在一个单独的头文件 Periods.mqh 中(将在 交易品种和时间范围章节中详述讨论)。
以相反顺序在 read 方法中执行读取(当然,假定读取将在一个不同的新对象中执行)。
bool read(int handle) override
|
如果文件中的任何特性(签名、交易品种、时间范围)与当前图表不匹配,则函数返回 false 以表明错误。
时间范围名称逆变换为 ENUM_TIMEFRAMES 枚举是由 StringToPeriod 函数完成,该函数同时是在 Periods.mqh 文件中。
以下是用于请求、保存和读取报价档案的主 Candles 类。
class Candles : public Persistent
|
字段是 FileHeader 类型的标头、请求的柱 limit 数量,以及从 MetaTrader 5 接收 MqlRates 结构体的数组。该数组在构造函数中填充。如果出错,limit 字段重置为零。
��生自 Persistent 接口的 Candles 类需要实现方法 write 和 read。在 write 方法中,我们首先指示头文件对象保存其自身,然后将报价数、日期范围(供参考)以及数组本身附加到文件。
bool write(int handle) override
|
读取则以逆顺序执行:
bool read(int handle) override
|
在用于存档报价的真实程序中,若存在日期范围,便能够按文件标头构建长期历史时段的正确系列,并且在一定程度上可防止对文件随意重命名。
有一个简单的 print 方法控制该过程:
void print() const
|
在脚本的主函数中,我们创建两个 Candles 对象,先用其中一个保存报价档案,然后用一个还原报价档案。这由我们已经了解过的包装器 FileHandle 管理(参见 文件描述符管理章节)。
const string filename = "MQL5Book/atomic.raw";
|
下面是一个 XAUUSD,H1 的初始数据日志示例:
FileOpen(filename,FILE_BIN|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ)=1 / ok CopyRates(_Symbol,_Period,0,limit,rates)=10 / ok FileWriteString(handle,signature,StringLen(signature))=11 / ok FileWriteInteger(handle,StringLen(_Symbol),CHAR_VALUE)=1 / ok FileWriteString(handle,_Symbol)=6 / ok FileWriteString(handle,PeriodToString(),3)=3 / ok FileWriteInteger(handle,limit)=4 / ok FileWriteLong(handle,rates[0].time)=8 / ok FileWriteLong(handle,rates[limit-1].time)=8 / ok [time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume] [0] 2021.08.17 15:00:00 1791.40 1794.57 1788.04 1789.46 8157 5 0 [1] 2021.08.17 16:00:00 1789.46 1792.99 1786.69 1789.69 9285 5 0 [2] 2021.08.17 17:00:00 1789.76 1790.45 1780.95 1783.30 8165 5 0 [3] 2021.08.17 18:00:00 1783.30 1783.98 1780.53 1782.73 5114 5 0 [4] 2021.08.17 19:00:00 1782.69 1784.16 1782.09 1782.49 3586 6 0 [5] 2021.08.17 20:00:00 1782.49 1786.23 1782.17 1784.23 3515 5 0 [6] 2021.08.17 21:00:00 1784.20 1784.85 1782.73 1783.12 2627 6 0 [7] 2021.08.17 22:00:00 1783.10 1785.52 1782.37 1785.16 2114 5 0 [8] 2021.08.17 23:00:00 1785.11 1785.84 1784.71 1785.80 922 5 0 [9] 2021.08.18 01:00:00 1786.30 1786.34 1786.18 1786.20 13 5 0 |
下面是一个恢复的数据的示例(别忘了,结构体是根据我们的假设性技术任务以截断形式保存的):
FileOpen(filename,FILE_BIN|FILE_READ|FILE_ANSI|FILE_SHARE_READ|FILE_SHARE_WRITE)=2 / ok FileReadString(handle,StringLen(signature))=CANDLES/1.1 / ok FileReadInteger(handle,CHAR_VALUE)=6 / ok FileReadString(handle,len)=XAUUSD / ok FileReadString(handle,3)=H1 / ok FileReadInteger(handle)=10 / ok FileReadLong(handle)=1629212400 / ok FileReadLong(handle)=1629248400 / ok [time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume] [0] 2021.08.17 15:00:00 1791.40 1794.57 1788.04 1789.46 0 0 0 [1] 2021.08.17 16:00:00 1789.46 1792.99 1786.69 1789.69 0 0 0 [2] 2021.08.17 17:00:00 1789.76 1790.45 1780.95 1783.30 0 0 0 [3] 2021.08.17 18:00:00 1783.30 1783.98 1780.53 1782.73 0 0 0 [4] 2021.08.17 19:00:00 1782.69 1784.16 1782.09 1782.49 0 0 0 [5] 2021.08.17 20:00:00 1782.49 1786.23 1782.17 1784.23 0 0 0 [6] 2021.08.17 21:00:00 1784.20 1784.85 1782.73 1783.12 0 0 0 [7] 2021.08.17 22:00:00 1783.10 1785.52 1782.37 1785.16 0 0 0 [8] 2021.08.17 23:00:00 1785.11 1785.84 1784.71 1785.80 0 0 0 [9] 2021.08.18 01:00:00 1786.30 1786.34 1786.18 1786.20 0 0 0 |
很容易确保数据得以正确存储和读取。现在我们看看它们在文件中的情况:
在外部程序中查看具有报价档案的二进制文件的内部结构体
其中,标头的各种字段以彩色高亮显示:签名、交易品种名称长度、交易品种名称、时间范围名称等等。