管理文件描述符

由于我们需要不断记住打开的文件,还要记得在退出函数时释放本地描述符,要是能把这一整个例行工作交给特殊对象处理,将会很高效。

这种做法在编程领域很普遍,称为资源获取即初始化 (RAII)。使用 RAII 可更轻松控制资源,确保它们处于正确状态。特别是,若打开文件(并为其创建一个所有者对象)的函数从若干个不同位置退出,这种做法尤其有效。

RAII 的范围不限于文件。在 对象类型模板章节中,我们创建了 AutoPtr 类,该类管理对象指针。它是此概念的另一个例子,因为指针同时也是资源(内存),不仅很容易导致内存泄露,而且在算法的多个不同分支中释放指针也会消耗资源。

文件包装器类还能以另一种方式发挥作用。文件 API 未提供通过描述符来获取文件名的函数(尽管内部肯定存在这样一种关系)。同时,在对象内部,我们可以存储该名称并实现我们自己的描述符绑定方式。

在最简单的情况下,我们需要某个类能存储文件描述符并且能在析构函数中自动关闭该描述符。FileHandle.mqh 文件中提供了一个实现示例。

class FileHandle
{
   int handle;
public:
   FileHandle(const int h = INVALID_HANDLE) : handle(h)
   {
   }
   
   FileHandle(int &holderconst int h) : handle(h)
   {
      holder = h;
   }
   
   int operator=(const int h)
   {
      handle = h;
      return h;
   }
   ...

两个构造函数以及一个重载赋值运算符确保一个对象绑定到一个文件(描述符)。第二个构造函数允许你将一个引用(从调用代码)传递给一个局部变量,局部变量将额外获取一个新的描述符。这将是相同描述符的一种外部别名,其可以常规方式用于其它函数调用中。

但你也可以不使用别名。在这些情况下,类定义运算符 '~',其返回内部 handle 变量的值。

   int operator~() const
   {
      return handle;
   }

最后,实现该类的核心目的是智能析构函数:

   ~FileHandle()
   {
      if(handle != INVALID_HANDLE)
      {
         ResetLastError();
         // will set internal error code if handle is invalid
         FileGetInteger(handleFILE_SIZE);
         if(_LastError == 0)
         {
            #ifdef FILE_DEBUG_PRINT
               Print(__FUNCTION__": Automatic close for handle: "handle);
            #endif
            FileClose(handle);
         }
         else
         {
            PrintFormat("%s: handle %d is incorrect, %s(%d)"
               __FUNCTION__handleE2S(_LastError), _LastError);
         }
      }
   }

在智能析构函数中,经过多次检查后,为受控的 handle 变量调用 FileClose。关键在于,该文件可以在程序中其它地方显式关闭,虽然有了该类之后不再需要这一操作。因此,当算法执行离开了定义 FileHandle 对象的块时调用析构函数,此时描述符可能已失效。为证实这点,使用对 FileGetInteger 函数的虚拟调用。这是虚拟的,因为它不会执行任何有用的操作。如果在调用后内部错误代码仍为 0,则描述符有效。

我们可以略过所有这些检查,直接编写以下内容:

   ~FileHandle()
   {
      if(handle != INVALID_HANDLE)
      {
         FileClose(handle);
      }
   }

如果描述符损坏,FileClose 将不会返回任何警告。但我们已添加了能够输出诊断信息的检查。

我们来以实操的方式测试 FileHandle 类:其测试脚本称为 FileHandle.mq5

const string dummy = "MQL5Book/dummy";
   
void OnStart()
{
   // creating a new file or open an existing one and reset it
   FileHandle fh1(PRTF(FileOpen(dummy
      FILE_TXT FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ))); // 1
   // another way to connect the descriptor via '='
   int h = PRTF(FileOpen(dummy
      FILE_TXT FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ)); // 2
   FileHandle fh2 = h;
   // and another supported syntax:
   // int f;
   // FileHandle ff(f, FileOpen(dummy,
   //    FILE_TXT | FILE_WRITE | FILE_SHARE_WRITE | FILE_SHARE_READ));
   
   // data is supposed to be written here
   // ...
   
   // close the file manually (this is not necessary; only done to demonstrate 
   // that the FileHandle will detect this and won't try to close it again)
   FileClose(~fh1); // operator '~' applied to an object returns a handle
   
   // descriptor handle in variable 'h' bound to object 'fh2' is not manually closed
   // and will be automatically closed in the destructor
}

根据日志中的输出,一切如计划正常运行:

   FileHandle::~FileHandle: Automatic close for handle: 2
   FileHandle::~FileHandle: handle 1 is incorrect, INVALID_FILEHANDLE(5007)

然而,如果有很多文件,为每个文件创建一个跟踪对象副本就没那么方便了。在这种情况下,最好是设计单个对象,收集给定环境(例如,在一个函数内)中的所有描述符。

这样一个类在 FileHolder.mqh 文件中实现,并显示在 FileHolder.mq5 脚本中。FileHolder 本身的一个副本会根据请求创建 FileOpener 类的辅助观察对象,该类与 FileHandle(尤其是析构函数)以及 handle 字段共享通用特性。

要通过 FileHolder打开一个文件,应使用其 FileOpen 方法(其签名与标准 FileOpen 函数的签名一致)。

class FileHolder
{
   static FileOpener *files[];
   int expand()
   {
      return ArrayResize(filesArraySize(files) + 1) - 1;
   }
public:
   int FileOpen(const string filenameconst int flags
                const ushort delimiter = '\t', const uint codepage = CP_ACP)
   {
      const int n = expand();
      if(n > -1)
      {
         files[n] = new FileOpener(filenameflagsdelimitercodepage);
         return files[n].handle;
      }
      return INVALID_HANDLE;
   }

所有 FileOpener 对象都会添加到 files 数组中以跟踪它们的生命周期。在同一位置,零值元素标记创建 FileHolder 对象的本地环境(代码块)的注册时刻。FileHolder 构造函数负责这一操作。

   FileHolder()
   {
      const int n = expand();
      if(n > -1)
      {
         files[n] = NULL;
      }
   }

我们已经知道,在程序执行期间会进入嵌套代码块(其调用函数)。如果它们要求管理本地文件描述符,则应在其中描述 FileHolder 对象(为每个代码块声明一个,或者不声明)。根据堆栈规则(先入后出),所有这些描述在 files 加总,然后当程序退出环境时以逆序释放。每次退出时都会调用析构函数。

   ~FileHolder()
   {
      for(int i = ArraySize(files) - 1i >= 0; --i)
      {
         if(files[i] == NULL)
         {
            // decrement array and exit
            ArrayResize(filesi);
            return;
         }
         
         delete files[i];
      }
   }

它的任务是移除数组中的所有 FileOpener 对象,直至遇到第一个零值元素,该零值元素指示上下文的边界(数组中的后续元素是来自另一个外部上下文的描述符)。

你可以自己研究整个类。

我们看看其在 FileHolder.mq5 测试脚本中用法。除了 OnStart 函数,还有 SubFunc。文件操作在两个上下文中均执行。

const string dummy = "MQL5Book/dummy";
   
void SubFunc()
{
   Print(__FUNCTION__" enter");
   FileHolder holder;
   int h = PRTF(holder.FileOpen(dummy
      FILE_BIN FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ));
   int f = PRTF(holder.FileOpen(dummy
      FILE_BIN FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ));
   // use h and f
   // ...
   // no need to manually close files and track early function exits
   Print(__FUNCTION__" exit");
}
 
void OnStart()
{
   Print(__FUNCTION__" enter");
   
   FileHolder holder;
   int h = PRTF(holder.FileOpen(dummy
      FILE_BIN FILE_WRITE FILE_SHARE_WRITE FILE_SHARE_READ));
   // writing data and other actions on the file by descriptor
   // ...
   /*
   int a[] = {1, 2, 3};
   FileWriteArray(h, a);
   */
   
   SubFunc();
   SubFunc();
   
 if(rand() >32000// simulate branching by conditions
   {
      // thanks to the holder we don't need an explicit call
      // FileClose(h);
      Print(__FUNCTION__" return");
      return// there can be many exits from the function
   }
   
   /*
     ... more code
   */
   
   // thanks to the holder we don't need an explicit call
   // FileClose(h);
   Print(__FUNCTION__" exit");
}

我们尚未手动关闭任何句柄,FileHolder 的实例会在析构函数中自动关闭句柄。

下面是一个日志输出示例:

OnStart enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=1 / ok
SubFunc enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=2 / ok
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=3 / ok
SubFunc exit
FileOpener::~FileOpener: Automatic close for handle: 3
FileOpener::~FileOpener: Automatic close for handle: 2
SubFunc enter
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=2 / ok
holder.FileOpen(dummy,FILE_BIN|FILE_WRITE|FILE_SHARE_WRITE|FILE_SHARE_READ)=3 / ok
SubFunc exit
FileOpener::~FileOpener: Automatic close for handle: 3
FileOpener::~FileOpener: Automatic close for handle: 2
OnStart exit
FileOpener::~FileOpener: Automatic close for handle: 1