简化模式下的文件读写

在用于读写数据的 MQL5 文件函数中,分为两个数量不对等的组。第一组包括两个函数:FileSaveFileLoad,用于在单次函数调用中以二进制模式读写数据。一方面,该方法具有不可否认的优势:简单;但另一方面,也存在某些局限性(下文详述)。在第二个大组中,所有文件函数均以不同的方式使用:要执行一个逻辑完整的读或写操作,必须按顺序调用若干函数。这看起来比较复杂,但却提供了灵活性以及对过程的控制。第二组函数使用特殊整数(文件描述符)操作,这些整数应使用 FileOpen 函数获取(参见 下一章节)。

我们看看这两个函数的正式描述,然后研究它们的示例 (FileSaveLoad.mq5)。

bool FileSave(const string filename, const void &data[], const int flag = 0)

该函数将传递的 data 数组的所有元素写到一个名为 filename 的二进制文件。filename 参数可不仅包含文件名,而且还包含若干嵌套层级的文件夹的名称:如果指定文件夹尚不存在,该函数将创建这些文件夹。如果文件存在,将被覆写(除非被另一个程序占用)。

如同 data 参数一样,可传递任何内置类型的数组,字符串除外。它还可以是包含内置类型字段的简单结构体数组,但字符串、动态数组和指针除外。也不支持类。

如果必要,flag 参数可包含预定义常量 FILE_COMMON,该常量表示创建一个文件并将其写入到所有终端的共用数据目录(Common/Files/)。如果未指定 flag 参数(对应于默认值 0),则文件被写入到常规数据目录(如果 MQL 程序在终端中运行)或者写入到测试代理目录(如果是在测试程序中)。在后两种情况下,在该目录中使用 MQL5/Files/ 沙盒,如本章开头部分所描述。

该函数返回操作成功 (true) 或出错 (false) 标志。

long FileLoad(const string filename, void &data[], const int flag = 0)

该函数将二进制文件 filename 的全部内容读取到指定的 data 数组。文件名可包括 MQL5/FilesCommon/Files 沙盒中的文件夹层次。

data 数组必须是除 string 以外的任何内置类型,或者是简单结构体类型(见上文)。

flag 参数控制要搜索和打开文件的目录的选择:默认情况下(默认值 0),该目录是标准沙盒,但是如果设置了 FILE_COMMON 值,则是由所有终端共享的沙盒。

该函数返回读取的项数,如果出错,则返回 -1。

请注意,请注意,文件中的数据按一个数组元素大小的块读取。如果文件大小不是元素大小的倍数,则跳过剩余数据(不读取)。例如,如果文件大小是 10 字节,将其读取到一个 double 类型(sizeof(double)=8)的数组中,则实际仅载入 8 个字节,即 1 个元素(该函数将返回 1)。文件末尾的剩余 2 字节将被忽略。

FileSaveLoad.mq5 脚本中,我们定义两个结构体用于测试。

struct Pair
{
   short xy;
};
  
struct Simple
{
   double d;
   int i;
   datetime t;
   color c;
   uchar a[10]; // fixed size array allowed
   bool b;
   Pair p;      // compound fields (nested simple structures) are also allowed
   
   // strings and dynamic arrays will cause a compilation error when used
   // FileSave/FileLoad: structures or classes containing objects are not allowed
   // string s;
   // uchar a[];
   
   // pointers are also not supported
   // void *ptr;
};

Simple 结构体包含大多数允许类型的字段,以及一个包含 Pair 结构体类型的复合字段。在 OnStart 函数中,我们填充一个 Simple 类型的小型数组。

void OnStart()
{
   Simple write[] =
   {
      {+1.0, -1D'2021.01.01', clrBlue, {'a'}, true, {100016000}},
      {-1.0, -2D'2021.01.01', clrRed,  {'b'}, true, {100016000}},
   };
   ...

我们将选择与 MQL5Book 子文件夹一起写入数据的文件,以便我们的试验不会与你的工作文件混合:

   const string filename = "MQL5Book/rawdata";

我们将一个数组写到一个文件,将其读入另一个数组,然后比较它们。

   PRT(FileSave(filenamewrite/*, FILE_COMMON*/)); // true
   
   Simple read[];
   PRT(FileLoad(filenameread/*, FILE_COMMON*/)); // 2
   
   PRT(ArrayCompare(writeread)); // 0

FileLoad 返回 2,即读取了 2 个元素(2 个结构体)。如果比较结果为 0,则表示数据匹配。你可以在自己喜欢的文件管理器中打开 MQL5/Files/MQL5Book 文件夹,然后确认存在 'rawdata' 文件(不建议使用文本编辑器查看其内容,建议使用支持二进制模式的查看器)。

在脚本中,我们进一步将读取的结构体数组转换为字节,并将它们以十六进制代码的形式输出到日志。这是一种内存转储,可让你了解什么是二进制文件。

   uchar bytes[];
   for(int i = 0i < ArraySize(read); ++i)
   {
      uchar temp[];
      PRT(StructToCharArray(read[i], temp));
      ArrayCopy(bytestempArraySize(bytes));
   }
   ByteArrayPrint(bytes);

结果:

 [00] 00 | 00 | 00 | 00 | 00 | 00 | F0 | 3F | FF | FF | FF | FF | 00 | 66 | EE | 5F | 
 [16] 00 | 00 | 00 | 00 | 00 | 00 | FF | 00 | 61 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 
 [32] 00 | 00 | 01 | E8 | 03 | 80 | 3E | 00 | 00 | 00 | 00 | 00 | 00 | F0 | BF | FE | 
 [48] FF | FF | FF | 00 | 66 | EE | 5F | 00 | 00 | 00 | 00 | FF | 00 | 00 | 00 | 62 | 
 [64] 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 01 | E8 | 03 | 80 | 3E | 

由于内置 ArrayPrint 函数不能打印十六进制格式,我们必须开发自己的函数 ByteArrayPrint(此处未提供其源代码,参见随附文件)。

接下来,我们别忘了,FileLoad 能够将数据载入任何类型的数组,因此我们将使用该函数直接把同一个文件读取到字节数组中。

   uchar bytes2[];
   PRT(FileLoad(filenamebytes2/*, FILE_COMMON*/)); // 78,  39 * 2
   PRT(ArrayCompare(bytesbytes2)); // 0, equality

成功比较两个字节数组可表明,FileLoad 可以按指定方式任意操作文件中的原始数据(文件中并未存储其包含 Simple 结构体数组的信息)。

这里务必要注意的是,由于字节类型的最小大小为 (1),因此它是任何文件大小的倍数。因此,任何文件始终能被完整读取到字节数组中,无任何剩余。这里 FileLoad 函数返回了数字 78(元素数量等于字节数)。这是该文件的大小(两个结构体,每个 39 字节)。

基本上来说,FileLoad 解读任何类型数据的能力需要编程人员的介入和校验。尤其是在脚本中,我们将相同文件读入结构数组 MqlDateTime。这当然是错误的,但却没有报错。

   MqlDateTime mdt[];
   PRT(sizeof(MqlDateTime)); // 32
   PRT(FileLoad(filenamemdt)); // 2
 // attention: 14 bytes left unread
   ArrayPrint(mdt);

结果包含一系列无意义的数字:

        [year]      [mon] [day]     [hour]    [min]    [sec] [day_of_week] [day_of_year]
[0]          0 1072693248    -1 1609459200        0 16711680            97             0
[1] -402587648    4096003     0  -20975616 16777215  6286950     -16777216    1644167168

因为 MqlDateTime 的大小是 32,所以一个 78 字节的文件只能包含两个这样的结构体,还剩余 14 个字节。存在剩余表示有问题。但即使没有剩余,也不保证执行的操作有意义,因为两个不同的大小可能纯属巧合地与文件长度成倍数关系,但含义并不相同。此外,不同含义的两个结构体可能具有相同大小,但这并不意味着它们彼此之间可以相互读写。

毫不意外,结构体数组 MqlDateTime 的日志显示奇怪的值,因为它实际上是完全不同的数据类型。

为了使读取操作更谨慎,脚本实现了 FileLoad 函数的类似函数 – MyFileLoad。我们将在后续章节中详细分析该函数及其配套函数 MyFileSave,届时将学习新的文件函数,并将它们用于对内部结构体 FileSave/FileLoad 进行建模。同时,请注意,在我们的版本中,我们能够检查文件中是否存在未读取剩余部分并显示警告。

最后,我们看看脚本中演示的更多的几个潜在错误。

   /*
  // compilation error, string type not supported here
   string texts[];
   FileSave("any", texts); // parameter conversion not allowed
   */
   
   double data[];
   PRT(FileLoad("any"data)); // -1
   PRT(_LastError); // 5004, ERR_CANNOT_OPEN_FILE

第一个错误发生在编译时(这是代码块被注释掉的原因),因为不允许字符串数组。

第二个错误是读取不存在的文件,这是 FileLoad 返回 -1 的原因。解释性错误代码可使用 GetLastError(或 _LastError)轻松获得。