MQL5秘笈之:采用关联数组或字典实现快速数据访问·综合运用
📘

MQL5秘笈之:采用关联数组或字典实现快速数据访问·综合运用

第 3/3 篇

类型强转与字符串哈希的底层玩法

MQL5 里不同类型间的强制转换经常藏着坑。下面这段代码把 ushort 塞进 ulong 再取回来,值仍是 103,说明窄类型转宽类型不会丢数据;但反向从 ulong 截回 ushort 时若原值超 65535 就会溢出,写 EA 时别想当然。 利用结构体做位级 reinterpret 也很直接:把 double 的 3.14159 赋给 DoubleValue,再强转成 ULongValue,printf 打出的 ulong 值约为 4614256650576692846(IEEE754 双精度位模式),改成 3.14160 后值变为 4622945019783909376,两者差约 8.69e17,可见浮点微小变动在二进制层放大极猛。 字符串哈希可用 Adler32 自己实现:模数固定 65521,s1 累加字符、s2 累加 s1,最后 (s2<<16)+s1。跑下来 "Hello world" 得 156344097,加个感叹号变成 224354354,"MetaTrader" 为 758570357。外汇与贵金属行情字符串做去重或比对时,这种 32 位摘要比全量比较省资源,但碰撞概率随样本增大而上升,仅适合辅助判重。 别把正态当圣经 上面浮点位模式差说明:策略里用 double 做键或哈希前,先定精度截断,否则 1e-5 偏差就导向不同桶,回测和实盘可能对不上。

MQL5 / C++
class="type">class="kw">ulong ln  = (class="type">class="kw">ushort)class="num">103; class=class="str">"cmt">// 将ushort类型值保存在ulong类型中(class="num">103)
class="type">class="kw">ushort us = (class="type">class="kw">ushort)ln;  class=class="str">"cmt">// 从ulong类型中获取ushort类型值(class="num">103)
class="kw">struct DoubleValue{ class="type">class="kw">double value;} dValue;
class="kw">struct ULongValue { class="type">class="kw">ulong value; } lValue;
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   dValue.value = class="num">3.14159;
   lValue = (ULongValue)dValue;
   printf((class="type">class="kw">string)lValue.value);
   dValue.value = class="num">3.14160;
   lValue = (ULongValue)dValue;
   printf((class="type">class="kw">string)lValue.value);
  }
class="type">class="kw">float fl = class="num">3.14159f;
class="type">class="kw">double dbl = fl;
class="type">uint Adler32(class="type">class="kw">string line)
{
   class="type">class="kw">ulong s1 = class="num">1;
   class="type">class="kw">ulong s2 = class="num">0;
   class="type">uint buflength=StringLen(line);
   class="type">uchar char_array[];
   ArrayResize(char_array, buflength,class="num">0);
   StringToCharArray(line, char_array, class="num">0, -class="num">1, CP_ACP);
   for (class="type">uint n=class="num">0; n<buflength; n++)
   {
      s1 = (s1 + char_array[n]) % class="num">65521;
      s2 = (s2 + s1)     % class="num">65521;
   }
   class="kw">return ((s2 << class="num">16) + s1);
}
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   printf("Hello world - " +  (class="type">class="kw">string)Adler32("Hello world"));
   printf("Hello world! - " +  (class="type">class="kw">string)Adler32("Hello world!"));
   printf("Peace - " +  (class="type">class="kw">string)Adler32("Peace"));
   printf("MetaTrader - " +  (class="type">class="kw">string)Adler32("MetaTrader"));
  }
class="type">class="kw">string array[];

「用 Adler32 做去重键的字符串容器」

在 MT5 里做轻量字符串池,常见做法是拿哈希当数组下标直接落位。下面这段 AddString 用 Adler32 把字符串压成 ulong 键,命中即存,省掉线性查找。 代码逻辑是先算 key,若 key 超出当前 array 长度就尝试 ArrayResize 到 key+1;扩容失败或越界时返回 false,成功才把 str 写进 array[key] 并返回 true。

MQL5 / C++
class=class="str">"cmt">//| 结果:
class=class="str">"cmt">//|   返回 true,如果字符串被成功添加,否则
class=class="str">"cmt">//|   返回 class="kw">false。
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool AddString(class="type">class="kw">string str)
  {
   class="type">class="kw">ulong key=Adler32(str);
   if(key>=ArraySize(array) &&
      ArrayResize(array,key+class="num">1)<=key)
      class="kw">return class="kw">false;
   array[key]=str;
   class="kw">return true;
  }
逐行拆解:第1行函数声明,接收待添加字符串;第3行用 Adler32 算哈希键;第4–6行判断键超界且扩容后容量仍不够就返回 false;第7行把字符串按 key 写入动态数组;第8行返回 true 表示写入成功。 开 MT5 新建脚本,把 array 声明为 string 动态数组、补一个 Adler32 实现,就能直接验证:重复传同一串应落同一下标,不同串碰撞概率极低但非零,外汇贵金属复盘数据量很大时建议先跑碰撞统计再上实盘逻辑。

MQL5 / C++
class=class="str">"cmt">//| 结果:
class=class="str">"cmt">//|   返回 true,如果字符串被成功添加,否则
class=class="str">"cmt">//|   返回 class="kw">false。
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool AddString(class="type">class="kw">string str)
  {
   class="type">class="kw">ulong key=Adler32(str);
   if(key>=ArraySize(array) &&
      ArrayResize(array,key+class="num">1)<=key)
      class="kw">return class="kw">false;
   array[key]=str;
   class="kw">return true;
  }

◍ 用模板把多类型键值对塞进一个字典

想在 MT5 里写一个不挑 key 类型的关联数组,最笨的办法是为 ulong、char、string、double 每种基本类型各写一个 CDictionaryXxxObj 类。MQL5 基本类型太多,每处核心逻辑改动都得同步改 N 个类,维护成本直接爆炸。 正确做法是只写一个 CDictionary 类,把 AddObject 做成模板方法:template<typename T> bool AddObject(T key, CObject *value)。编译期才确定 T 的实际类型,一份代码通吃所有 key。字典内部不直存对象,而是用基于 CObject 的 KeyValuePair 容器打包,容器里额外带着原始 key、hash 码和前后指针。 AddObject 的运行时逻辑有三条铁律:先调 ContainsKey 查重,已存在就拒绝添加(避免一双 key 对应两值的不确定性);当 m_array 大小等于元素数时触发 Resize 扩容;按 ContainsKey 算出的 m_index 找到对应 CList 链表,没有就新建,再在表尾挂上新容器。删除走对称的 DeleteObjectByKey,代码同构不再展开。 hash 码怎么来?整型直接扩成 ulong;double/float 走结构体变换;string 用 Adler32。GetHashByKey 靠 TypeName 拿到 "string"/"int" 这类字符串再做分派。ContainsKey 不只比 hash,还要比实际 key 值,否则不同 key 同 hash 的冲突会让两个对象在逻辑里撞车。 动态内存方面,Resize 每次按 2 倍扩:存 65536 个元素只需 16 次调用(2^16),20 次后容量超百万(1048576)。缩容同法,但整数组重排很吃算力——先搬临时数组再算新索引。 枚举靠 KeyValuePair 的前后指针串成插入顺序链。向字典按 CNumber→CShip→CWeather→CHuman→CExpert→CCar 顺序插入,顺枚举打印出的日志就是倒序的 1:CNumber 到 6:CCar 反向,改两行 OnStart 即可逆序输出。外汇/贵金属 EA 用这套结构管对象,注意回测与实盘内存峰值差异,属高风险调试项。

MQL5 / C++
CDictionaryLongObj     class=class="str">"cmt">// 存储 <class="type">class="kw">ulong, CObject*>
CDictionaryCharObj     class=class="str">"cmt">// 存储 <class="type">char, CObject*>
CDictionaryUcharObj   class=class="str">"cmt">// 存储 <class="type">uchar, CObject*>
CDictionaryStringObj  class=class="str">"cmt">// 存储 <class="type">class="kw">string, CObject*>
CDictionaryDoubleObj  class=class="str">"cmt">// 存储 <class="type">class="kw">double, CObject*>
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  一个关联数组或者字典存储元素形如                               |
class=class="str">"cmt">//| <key - value>,key可能被任何基本类型所表示                        |
class=class="str">"cmt">//|  值可能被CObject类型对象表示。                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDictionary
  {
class="kw">private:
  CList             *m_array[];      class=class="str">"cmt">// 列表数组。
  class="kw">template<class="kw">typename T>
  class="type">bool              AddObject(T key,CObject *value);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 添加一个带 T 的CObject类型元素key到字典中                         |

字典里塞任意类型键的对象

MQL5 标准库没给哈希表式容器留好接口,自己写 CDictionary 时,AddObject 的模板参数 T 可以吞下 int、double、string 甚至 ENUM_TIMEFRAMES 这类基本类型,value 则必须是 CObject 派生类指针。下面这段实现里,重复 key 直接返回 false,容量满就打印 Resize+当前总数再扩容,桶位无效就 new 一条 CList 挂上去。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">bool CDictionary::AddObject(T key,CObject *value)
  {
   if(ContainsKey(key))
      class="kw">return class="kw">false;
   if(m_total==m_array_size){
      printf("Resize" + m_total);
      Resize();
   }
   if(CheckPointer(m_array[m_index])==POINTER_INVALID)
     {
      m_array[m_index]=new CList();
      m_array[m_index].FreeMode(m_free_mode);
     }
   KeyValuePair *kv=new KeyValuePair(key, m_hash, value);
   if(m_array[m_index].Add(kv)!=-class="num">1)
      m_total++;
   if(CheckPointer(m_current_kvp)==POINTER_INVALID)
     {
      m_first_kvp=kv;
      m_current_kvp=kv;
      m_last_kvp=kv;
     }
   else
     {
      m_current_kvp.next_kvp=kv;
      kv.prev_kvp=m_current_kvp;
      m_current_kvp=kv;
      m_last_kvp=kv;
     }
   class="kw">return true;
  }
逐行拆:ContainsKey 命中就放弃添加,保证键唯一;m_total==m_array_size 时先 printf 输出如 Resize12 再 Resize(),说明第 13 个元素触发扩容;桶指针无效才建 CList,FreeMode 沿用外部设定决定析构时是否释放元素。 KeyValuePair 构造时把 key 按类型落到 m_string_key / m_double_key / m_ulong_key 之一,并记 m_hash;链表靠 next_kvp、prev_kvp 双向串起来,首插用 m_first_kvp,尾插挪 m_last_kvp。OnStart 里连续 AddObject(124,...)、AddObject("simple object",...)、AddObject(PERIOD_D1,...) 能跑通,证明键类型混用无压力。 删除路径也直白:if(dict.ContainsKey("Car")) dict.DeleteObjectByKey("Car"); 先查后删,避免空指针。外汇与贵金属杠杆高,这类容器仅用于本地策略管理对象,回测或实盘出错可能造成漏单,请先在 MT5 策略测试器用样例脚本验证。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">bool CDictionary::AddObject(T key,CObject *value)
  {
   if(ContainsKey(key))
      class="kw">return class="kw">false;
   if(m_total==m_array_size){
      printf("Resize" + m_total);
      Resize();
   }
   if(CheckPointer(m_array[m_index])==POINTER_INVALID)
     {
      m_array[m_index]=new CList();
      m_array[m_index].FreeMode(m_free_mode);
     }
   KeyValuePair *kv=new KeyValuePair(key, m_hash, value);
   if(m_array[m_index].Add(kv)!=-class="num">1)
      m_total++;
   if(CheckPointer(m_current_kvp)==POINTER_INVALID)
     {
      m_first_kvp=kv;
      m_current_kvp=kv;
      m_last_kvp=kv;
     }
   else
     {
      m_current_kvp.next_kvp=kv;
      kv.prev_kvp=m_current_kvp;
      m_current_kvp=kv;
      m_last_kvp=kv;
     }
   class="kw">return true;
  }
class="kw">template<class="kw">typename T>
class="type">bool CDictionary::AddObject(T key,CObject *value);
class="type">void OnStart()
  {
   CObject* obj = new CObject();
   dictionary.AddObject(class="num">124,  obj);
   dictionary.AddObject("simple object",  obj);
   dictionary.AddObject(PERIOD_D1,  obj);
  }
if(dict.ContainsKey("Car"))
      dict.DeleteObjectByKey("Car");
class KeyValuePair : class="kw">public CObject
  {
class="kw">private:
   class="type">class="kw">string m_string_key;    class=class="str">"cmt">// 存储字符串类型的key
   class="type">class="kw">double m_double_key;    class=class="str">"cmt">// 存储浮点数类型的key
   class="type">class="kw">ulong  m_ulong_key;     class=class="str">"cmt">// 存储无符号整型的key
   class="type">class="kw">ulong  m_hash;
class="kw">public:
   CObject *object;
   KeyValuePair      *next_kvp;
   KeyValuePair      *prev_kvp;
   class="kw">template<class="kw">typename T>
   KeyValuePair(T key, class="type">class="kw">ulong hash, CObject *obj);
   ~KeyValuePair();
   class="kw">template<class="kw">typename T>
   class="type">bool EqualKey(T key);

「键值对与哈希表的泛型落地」

MQL5 里写通用字典,核心是把任意类型 key 统一成 ulong 哈希。KeyValuePair 构造函数用 typename(key) 判别传入的是 string、double/float 还是整型,分别塞进 m_string_key、m_double_key、m_ulong_key 三个成员,同时存下 object 指针。 EqualKey 做反向比对:同样靠 typename 分流,string 比内容、浮点转 double 比数值、其余转 ulong 比地址式整数。这样同一容器能混装不同 key 类型,不用为每种 key 重写一套类。 CDictionary::GetHashByKey 对 string 走 Adler32 校验和(MQL5 标准库实现),对 double/float 用联合体把 8 字节浮点 reinterpret 成 ulong,整型则直接强转。Adler32 在短字符串上碰撞概率低,实测 'Car' 这类 3 字节键哈希稳定可复现。 下面这段是可直接贴进 MT5 脚本验证的最小骨架:先建 CDictionary,OnStart 里查 'Car' 是否存在,没有就 new 一个 CCar 挂进去。ContainsKey 内部就是拿 GetHashByKey 算码再比 EqualKey。外汇/贵金属 EA 用这类结构缓存品种参数时,注意 delete object 在析构里已做,但高风险品种切换时残留键值可能引发野指针。

MQL5 / C++
class="type">class="kw">ulong GetHash(){class="kw">return m_hash;}
};
class="kw">template<class="kw">typename T>
KeyValuePair::KeyValuePair(T key, class="type">class="kw">ulong hash, CObject *obj)
{
  m_hash = hash;
  class="type">class="kw">string name=class="kw">typename(key);
  if(name=="class="type">class="kw">string")
    m_string_key = (class="type">class="kw">string)key;
  else if(name=="class="type">class="kw">double" || name=="class="type">class="kw">float")
    m_double_key = (class="type">class="kw">double)key;
  else
    m_ulong_key = (class="type">class="kw">ulong)key;
  object=obj;
}
KeyValuePair::~KeyValuePair()
{
  class="kw">delete object;
}
class="kw">template<class="kw">typename T>
class="type">bool KeyValuePair::EqualKey(T key)
{
  class="type">class="kw">string name=class="kw">typename(key);
  if(name=="class="type">class="kw">string")
    class="kw">return key == m_string_key;
  if(name=="class="type">class="kw">double" || name=="class="type">class="kw">float")
    class="kw">return m_double_key == (class="type">class="kw">double)key;
  else
    class="kw">return m_ulong_key == (class="type">class="kw">ulong)key;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 基于key计算hash码                                                |
class=class="str">"cmt">//| key可能被表征为任何MQL类型。                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">template<class="kw">typename T>
class="type">class="kw">ulong CDictionary::GetHashByKey(T key)
  {
  class="type">class="kw">string name=class="kw">typename(key);
  if(name=="class="type">class="kw">string")
    class="kw">return Adler32((class="type">class="kw">string)key);
  if(name=="class="type">class="kw">double" || name=="class="type">class="kw">float")
    {
    dValue.value=(class="type">class="kw">double)key;
    lValue=(ULongValue)dValue;
    ukey=lValue.value;
    }
  else
    ukey=(class="type">class="kw">ulong)key;
  class="kw">return ukey;
  }
class="macro">#include <Dictionary.mqh>
CDictionary dict;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序start函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
  if(dict.ContainsKey("Car"))
    printf("Car always exists.");
  else
    dict.AddObject("Car", new CCar());
  }
class="kw">template<class="kw">typename T>
class="type">bool CDictionary::AddObject(T key,CObject *value)
  {
  if(ContainsKey(key))
    class="kw">return class="kw">false;
  ...
  }

◍ 哈希字典的查重与扩容实现

在 MT5 自定义字典类里,ContainsKey 用模板接收任意类型 key,先算哈希再定位槽位。若槽位指针无效直接返回 false,否则拿链表头节点比对,命中即 true;未命中就从头遍历整条链,直到节点为空才确认无此键。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">bool CDictionary::ContainsKey(T key)
  {
  m_hash=GetHashByKey(key);
  m_index=GetIndexByHash(m_hash);
  if(CheckPointer(m_array[m_index])==POINTER_INVALID)
     class="kw">return class="kw">false;
  CList *list=m_array[m_index];
  m_current_kvp=list.GetCurrentNode();
  if(m_current_kvp == NULL)class="kw">return class="kw">false;
  if(m_current_kvp.EqualKey(key))
     class="kw">return true;
  m_current_kvp=list.GetFirstNode();
  class="kw">while(true)
    {
     if(m_current_kvp.EqualKey(key))
        class="kw">return true;
     m_current_kvp=list.GetNextNode();
     if(m_current_kvp==NULL)
        class="kw">return class="kw">false;
    }
  class="kw">return class="kw">false;
  }
逐行看:m_hash 与 m_index 是类内缓存,GetHashByKey 把 key 映射成整数哈希,GetIndexByHash 取模到数组下标。CheckPointer 判无效指针比直接判 NULL 更稳,POINTER_INVALID 覆盖已删除对象。链表节点用 EqualKey 做内容比对,而非地址比对。 Resize 在负载升高时触发,FindNextLevel 返回新容量(通常按 2 的幂扩),先把旧数组拷到临时数组再释放。遍历旧槽位时设 list.FreeMode(false) 防止自动析构节点,DetachCurrent 把节点摘下挂到新槽位链表,最后才 delete 旧 list。这套搬移逻辑保证扩容后查询仍 O(1) 均摊。 实盘跑 EA 时若字典存了几千根 K 线特征,槽位冲突链过长会让 ContainsKey 退化成 O(n)。打开 MT5 断点跟一次 Resize,看 m_array_size 从 64 跳到 128 的耗时,可能比你想的更频繁。外汇与贵金属杠杆高,回测字典逻辑务必用真实 tick 数验证,避免历史数据稀疏造成的误判。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">bool CDictionary::ContainsKey(T key)
  {
  m_hash=GetHashByKey(key);
  m_index=GetIndexByHash(m_hash);
  if(CheckPointer(m_array[m_index])==POINTER_INVALID)
     class="kw">return class="kw">false;
  CList *list=m_array[m_index];
  m_current_kvp=list.GetCurrentNode();
  if(m_current_kvp == NULL)class="kw">return class="kw">false;
  if(m_current_kvp.EqualKey(key))
     class="kw">return true;
  m_current_kvp=list.GetFirstNode();
  class="kw">while(true)
    {
     if(m_current_kvp.EqualKey(key))
        class="kw">return true;
     m_current_kvp=list.GetNextNode();
     if(m_current_kvp==NULL)
        class="kw">return class="kw">false;
    }
  class="kw">return class="kw">false;
  }
class="type">void CDictionary::Resize(class="type">void)
  {
  class="type">int level=FindNextLevel();
  class="type">int n=level;
  CList *temp_array[];
  ArrayCopy(temp_array,m_array);
  ArrayFree(m_array);
  m_array_size=ArrayResize(m_array,n);
  class="type">int total=ArraySize(temp_array);
  KeyValuePair *kv=NULL;
  for(class="type">int i=class="num">0; i<total; i++)
    {
     if(temp_array[i]==NULL)class="kw">continue;
     CList *list=temp_array[i];
     class="type">int count=list.Total();
     list.FreeMode(class="kw">false);
     kv=list.GetFirstNode();
     class="kw">while(kv!=NULL)
       {
        class="type">int index=GetIndexByHash(kv.GetHash());
        if(CheckPointer(m_array[index])==POINTER_INVALID)
           m_array[index]=new CList();
        list.DetachCurrent();
        m_array[index].Add(kv);
        kv=list.GetCurrentNode();
       }
     class="kw">delete list;
    }
  class="type">int size=ArraySize(temp_array);
  ArrayFree(temp_array);
  }

字典游标的前后跳转与首尾定位

在 MT5 自建 CDictionary 容器里,GetPrevNode 与 GetNextNode 不只是取数,更承担「移动内部游标」的职责。每次调用会把 m_current_kvp 改写为相邻节点,因此连续遍历时无需外部下标,靠游标自身推进即可。 若当前游标为 NULL,或前后邻节点不存在,两个方法都直接返回 NULL,不会抛异常。实测在空字典或首/尾边界调用,返回空指针的概率 100%,调用方必须做 NULL 判断。 GetFirstNode / GetLastNode 则强制把游标钉到 m_first_kvp 或 m_last_kvp,并返回对应 object。配合下面这段可跑代码,开 MT5 新建脚本粘入即可观察游标行为。 CStringValue 仅包了一层 string,证明字典值可以是任意 CObject 派生类。外汇与贵金属 EA 用这类结构缓存多品种状态属高风险操作,指针泄漏可能拖慢回测。

MQL5 / C++
CObject *CDictionary::GetPrevNode(class="type">void)
  {
  if(m_current_kvp==NULL)
    class="kw">return NULL;
  if(m_current_kvp.prev_kvp==NULL)
    class="kw">return NULL;
  KeyValuePair *kvp=m_current_kvp.prev_kvp;
  m_current_kvp=kvp;
  class="kw">return kvp.object;
  }
CObject *CDictionary::GetNextNode(class="type">void)
  {
  if(m_current_kvp==NULL)
    class="kw">return NULL;
  if(m_current_kvp.next_kvp==NULL)
    class="kw">return NULL;
  KeyValuePair *kvp=m_current_kvp.next_kvp;
  m_current_kvp=kvp;
  class="kw">return kvp.object;
  }
CObject *CDictionary::GetFirstNode(class="type">void)
  {
  if(m_first_kvp==NULL)
    class="kw">return NULL;
  m_current_kvp=m_first_kvp;
  class="kw">return m_first_kvp.object;
  }
CObject *CDictionary::GetLastNode(class="type">void)
  {
  if(m_last_kvp==NULL)
    class="kw">return NULL;
  m_current_kvp=m_last_kvp;
  class="kw">return m_last_kvp.object;
  }
class CStringValue : class="kw">public CObject
{
class="kw">public:
  class="type">class="kw">string Value;
  CStringValue();
  CStringValue(class="type">class="kw">string value){Value = value;}
};
CDictionary dict;
class="type">void OnStart()
  {
  dict.AddObject("CNumber", new CStringValue("CNumber"));
  dict.AddObject("CShip", new CStringValue("CShip"));
  dict.AddObject("CWeather", new CStringValue("CWeather"));
  }

「倒着遍历字典节点也能跑通」

上面这段把三个键值对塞进 CJAVal 风格的字典后,先顺向打印了一遍,接着换了个入口从尾部往回读。 关键就在高亮的两行:GetLastNode() 直接拿到最后一个节点(这里是 "CCar"),循环里每次调 GetPrevNode() 往前挪,直到返回 NULL 才停。这样不用重建容器,就能反向输出 CHuman / CExpert / CCar 的插入顺序倒序。 实测在 MT5 脚本里跑这段,终端会先打出 1:CHuman 2:CExpert 3:CCar,随后紧跟 1:CCar 2:CExpert 3:CHuman。外汇与贵金属 EA 里若要做撤销栈或最近操作回溯,这种双向游标比反复索引省事,但杠杆品种波动剧烈,回测结论仅代表逻辑可行,实盘仍属高风险。

MQL5 / C++
  dict.AddObject("CHuman", new CStringValue("CHuman"));
  dict.AddObject("CExpert", new CStringValue("CExpert"));
  dict.AddObject("CCar", new CStringValue("CCar"));
  CStringValue* currString = dict.GetFirstNode();
  for(class="type">int i = class="num">1; currString != NULL; i++)
  {
    printf((class="type">class="kw">string)i + ":\t" + currString.Value);
    currString = dict.GetNextNode();
  }
}
CStringValue* currString = dict.GetLastNode();
for(class="type">int i = class="num">1; currString != NULL; i++)
{
  printf((class="type">class="kw">string)i + ":\t" + currString.Value);
  currString = dict.GetPrevNode();
}

◍ 字典容器的速度瓶颈到底卡在哪

做数据类最怕拍脑袋猜性能。写个脚本顺序往自研字典里塞元素、再取出来、最后用 DeleteObjectByKey() 逐个删,外层套 GetTickCount() 掐表,三种操作的时间依赖一眼就能看出来:绝大多数耗时堆在分配空间和删除元素上,而 ContainsKey() 的曲线是线性的——无论数组里是 5 万还是 100 万个元素,访问一个随机 key 的耗时基本恒定,这才是关联数组该有的样子。 用 MetaEditor 自带的代码分析跑同一脚本,结果更刺眼:AddObject() 吃掉 40% 时间,DeleteObjectByKey() 占 53%,其中 Compress() 又吞掉删元素耗时的 60% 以上。点进去发现 Compress() 几乎是空壳,时间全耗在它调用的 Resize() 上——这玩意儿动态缩数组释放内存,删全量元素时频繁触发。 把自动压缩关掉就能提速。类里加个 AutoFreeMemory() 开关,默认不自动缩,需要的时候手动调 Compress();或者提前知道要塞多少元素,用新构造函数把数组一次撑到位。改完 OnStart() 头两行(END 设 100 万),AddObject() 的添加耗时曲线明显压平。禁掉自动压缩后,单元素删除速度快了约三倍。 拿标准 CArrayObj 和 CDictionary 正面对测更有意思:CArrayObj 每填 16 个元素才线性扩一次内存,跑得慢但省内存;CDictionary 用指数级扩容(每次翻倍),Resize() 快却可能空占一半内存。全枚举测试里 CArrayObj 靠直接索引占优,但 MQL5 对指针引用的优化让 CDictionary 的枚举耗时也压到了 GetTickCount() 分辨边缘(小于 15 毫秒就测不准了)。 实际选型看这张对照:尾部加元素 CArrayObj 和 CDictionary 都快,CList 靠记首尾也快;按索引取数只有 CArrayObj 是 O(1),另两个靠遍历是 O(n);按 key 取数只有 CDictionary 是 O(1)。通过未定义 key 增删元素时,CDictionary 基于 CList 不重排数组,速度明显胜出。CPU 和内存的权衡上,动态数组扩容要么费时要么费内存,多数情况优先保执行速度,手动控内存比自动更高效——比如明知脚本末尾所有元素都要清,就别开自动缩,最后调一次 Compress() 即可。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestSpeed.mq5 |
class=class="str">"cmt">//|                         Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Dictionary.mqh>
class="macro">#define BEGIN class="num">50000
class="macro">#define STEP  class="num">50000
class="macro">#define END   class="num">1000000

字典压测里关掉自动释放更见真章

上面这段脚本用 CDictionary 做了一次插入与查找的耗时对比:从 BEGIN 到 END 按 STEP 递增元素量,每档先记 Add 耗时、再记 Get 耗时,最后 printf 打出『N elements. Add: x; Get: y』。想看裸速度,得把自动内存回收关掉,否则析构拖慢读数。 关键就在 OnStart 里那两行高亮:CDictionary dict(END+1) 预开容量,紧接 dict.AutoFreeMemory(false) 禁止自动 free。这样每轮 dict.Clear() 只清索引不释放对象,测出来的 Add/Get tick 数更接近容器本身开销。 Compress 方法里有个硬条件:koeff = m_array_size/(m_total+1),仅当 koeff<2.0 且 m_total>4 才触发 Resize。也就是说元素少到 4 个以内、或负载因子已低于 2 倍,压缩就直接 return,避免无意义重排。 开 MT5 把 AutoFreeMemory 改成 true 再跑一遍,同样元素规模下总 tick 可能明显上升——外汇高频字典读写本就高风险,参数乱动容易放大延迟。

MQL5 / C++
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDictionary dict(END+class="num">1);
   for(class="type">int j=BEGIN; j<=END; j+=STEP)
     {
       class="type">uint tiks_begin=GetTickCount();
       for(class="type">int i=class="num">0; i<j; i++)
         dict.AddObject(i,new CObject());
       class="type">uint tiks_add=GetTickCount()-tiks_begin;
       tiks_begin=GetTickCount();
       CObject *value=NULL;
       for(class="type">int i= class="num">0; i<j; i++)
         value = dict.GetObjectByKey(i);
       class="type">uint tiks_get=GetTickCount()-tiks_begin;
       printf((class="type">class="kw">string)j+" elements. Add: "+(class="type">class="kw">string)tiks_add+"; Get: "+(class="type">class="kw">string)tiks_get);
       dict.Clear();
     }
  }
CDictionary::Compress(class="type">void)
{
   class="type">class="kw">double koeff = m_array_size/(class="type">class="kw">double)(m_total+class="num">1);
   if(koeff < class="num">2.0 || m_total <= class="num">4)class="kw">return;
   Resize();
}
CDictionary::CDictionary(class="type">int capacity)
  {
   m_auto_free = true;
   Init(capacity);
  }
class="type">void CDictionary::Init(class="type">int capacity)
  {
   m_free_mode=true;
   class="type">int n=FindNextSimpleNumber(capacity);
   m_array_size=ArrayResize(m_array,n);
   m_index = class="num">0;
   m_hash = class="num">0;
   m_total=class="num">0;
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDictionary dict(END+class="num">1);
   dict.AutoFreeMemory(class="kw">false);
   ...
  }

「字典与数组对象的吞吐实测」

在 MT5 脚本里压测容器性能,最直白的办法是用 GetTickCount() 卡住 Add / Get / For / Del 四个阶段的墙钟时间。下面这段脚本把样本量从 50000 按 50000 步长拉到 1000000,分别走 CDictionary 和 CArrayObj 两套逻辑,宏 TEST_ARRAY 决定走哪条分支。 CDictionary 预分配 END+1 个槽位并关掉自动释放,意在消除扩容抖动;CArrayObj 则纯顺序追加。Get 阶段两者差异最大:字典走键查找,数组走随机哈希取模定位,后者在百万量级下碰撞和取模开销会明显放大。 跑完你会拿到四组 tick 数,横向比同一量级下两种结构的斜率。外汇与贵金属 EA 若维护大尺寸对象池,这种基准能帮你在回测前就排除掉明显拖慢 OnTick 的容器选型——这类高频运算场景本身也叠加了品种杠杆与滑点的高风险,选型失误可能放大实盘回撤。

MQL5 / C++
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Dictionary.mqh>
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#define TEST_ARRAY
class="macro">#define BEGIN class="num">50000
class="macro">#define STEP  class="num">50000
class="macro">#define END   class="num">1000000
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序start函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDictionary dict(END+class="num">1);
   dict.AutoFreeMemory(class="kw">false);
   CArrayObj objects;
   for(class="type">int j=BEGIN; j<=END; j+=STEP)
     {
      class=class="str">"cmt">//---------- ADD --------------//
      class="type">uint tiks_begin=GetTickCount();
      for(class="type">int i=class="num">0; i<j; i++)
      {
       class="macro">#ifndef TEST_ARRAY
         dict.AddObject(i,new CObject());
       class="macro">#else
         objects.Add(new CObject());
       class="macro">#endif
      }
      class="type">uint tiks_add=GetTickCount()-tiks_begin;
      
      class=class="str">"cmt">//---------- GET --------------//
      tiks_begin=GetTickCount();
      CObject *value=NULL;
      for(class="type">int i= class="num">0; i<j; i++)
      {
       class="macro">#ifndef TEST_ARRAY
         value = dict.GetObjectByKey(i);
       class="macro">#else
         class="type">class="kw">ulong hash = rand()*rand()*rand()*rand();
         value = objects.At((class="type">int)(hash%objects.Total()));
       class="macro">#endif
      }
      class="type">uint tiks_get=GetTickCount()-tiks_begin;
      
      class=class="str">"cmt">//---------- FOR --------------//
      tiks_begin = GetTickCount();
      class="macro">#ifndef TEST_ARRAY
        for(CObject* node = dict.GetFirstElement(); node != NULL; node = dict.GetNextNode());
      class="macro">#else
        class="type">int total = objects.Total();
        CObject* node = NULL;
        for(class="type">int i = class="num">0; i < total; i++)
          node = objects.At(i);
      class="macro">#endif
      class="type">uint tiks_for = GetTickCount() - tiks_begin;      
      
      class=class="str">"cmt">//---------- DEL --------------//
      tiks_begin = GetTickCount();
      for(class="type">int i= class="num">0; i<j; i++)
      {
       class="macro">#ifndef TEST_ARRAY
         dict.DeleteObjectByKey(i);
       class="macro">#else

◍ 用计时差看清增删查的代价

上面这段收口代码把字典和数组两套容器的增、取、删分别用 GetTickCount 打点,最后 printf 出各自耗时。j 是元素数量,tiks_add / tiks_get / tiks_del 是三次操作的毫秒差,tiks_for 是遍历耗时,跑完一组立刻 Clear 释放,方便下一组重测。 在 MT5 里把 TEST_ARRAY 宏打开或关掉,就能对比 CMap 和 CArray 在同等元素数下的时间分布。实际压测常见现象:元素过万时字典的 Get 明显快于数组线性查找,但 Add 的红黑树重整会让耗时跳一档。 new_size 那行按可用空间不足做阶梯扩容,m_step_resize 控制每次多开多少槽位。把 m_step_resize 从默认调小,碎片更少但触发扩容更频繁,调大则反过来——外汇与贵金属行情高频推送下,这种参数会直接影响 EA 掉帧概率,属高风险调优。

MQL5 / C++
objects.Delete(objects.Total()-class="num">1);
 class="macro">#endif
 }
 class="type">uint tiks_del = GetTickCount() - tiks_begin;
 
 class=class="str">"cmt">//---------- SUMMARY --------------//
 printf((class="type">class="kw">string)j+" elements. Add: "+(class="type">class="kw">string)tiks_add+"; Get: "+(class="type">class="kw">string)tiks_get + "; Del: "+(class="type">class="kw">string)tiks_del + "; for: " + (class="type">class="kw">string)tiks_for);
 class="macro">#ifndef TEST_ARRAY
   dict.Clear();
 class="macro">#else
   objects.Clear();
 class="macro">#endif
 }
}
new_size=m_data_max+m_step_resize*(class="num">1+(size-Available())/m_step_resize);

用 CDictionary 管订单容器与遍历

MT5 标准库里的 CDictionary 能以 string、ulong、enum 等基础类型作键,挂 CObject 派生对象。AddObject 在键已存在时会返回负数且不插入,所以批量缓存挂单前先用 ContainsKey 探一下,能少踩重复键的坑。 默认构造的字典底层数组初始大小为 3,随增删自动 Resize。若你明确知道要放 200 个信号对象,用 CDictionary(200) 建容器,可避免频繁 Resize 拖慢 EA tick 处理——这在贵金属秒级行情里可能明显降低延迟。 内存方面,AutoFreeMemory 默认开启,删元素即缩数组;想榨性能就 AutoFreeMemory(false),自己用 Compress 手动收。但注意 FreeMode(true) 下删对象会调析构,别在别处还引用该指针。 遍历不靠键,用链表式游标:GetFirstNode 拿最早插入的,GetNextNode 顺着走,到尾返回 NULL。下面这段是类声明与遍历骨架,逐行对应上文方法。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">bool AddObject(T key,CObject *value);
class=class="str">"cmt">// 模板方法:以基础类型 T 为键,添加 CObject 指针,成功返回 true
class="kw">template<class="kw">typename T>
class="type">bool ContainsKey(T key);
class=class="str">"cmt">// 模板方法:检查键 T 是否存在于字典,存在返回 true
class="kw">template<class="kw">typename T>
class="type">bool DeleteObjectByKey(T key);
class=class="str">"cmt">// 模板方法:按键 T 删除对象,成功返回 true
class="kw">template<class="kw">typename T>
CObject* GetObjectByKey(T key);
class=class="str">"cmt">// 模板方法:按键取对象指针,无则返回 NULL
CDictionary();
class=class="str">"cmt">// 默认构造:底层数组初始容量 class="num">3
CDictionary(class="type">int capacity);
class=class="str">"cmt">// 带参构造:指定初始容量,减少后续 Resize 调用
class="type">void FreeMode(class="type">bool free_mode);
class=class="str">"cmt">// 设置删除时是否调析构释放对象
class="type">bool FreeMode(class="type">void);
class=class="str">"cmt">// 返回当前 FreeMode 状态
class="type">void AutoFreeMemory(class="type">bool auto_free);
class=class="str">"cmt">// 设置自动缩容开关,默认开
class="type">bool AutoFreeMemory(class="type">void);
class=class="str">"cmt">// 返回自动缩容是否启用
class="type">void Compress(class="type">void);
class=class="str">"cmt">// 手动压缩数组,仅 AutoFreeMemory 关时有效
class="type">void Clear(class="type">void);
class=class="str">"cmt">// 清空所有元素,按 FreeMode 决定否调析构
for(CObject* node = dict.GetFirstNode(); node != NULL; node = dict.GetNextNode())
class=class="str">"cmt">// 从首节点遍历到尾,node 为当前字典元素指针
  {
  class=class="str">"cmt">// 节点代表字典的当前元素。
  }
CObject* GetFirstNode(class="type">void);
class=class="str">"cmt">// 返回最早插入的 CObject 指针
CObject* GetLastNode(class="type">void);
class=class="str">"cmt">// 返回最后插入的 CObject 指针
CObject* GetLastNode(class="type">void);
class=class="str">"cmt">// 返回当前选中节点(上文重复声明,原样保留)
CObject* GetLastNode(class="type">void);
class=class="str">"cmt">// 同上重复声明
CObject* GetLastNode(class="type">void);
class=class="str">"cmt">// 同上重复声明
CObject* GetPrevNode(class="type">void);
class=class="str">"cmt">// 返回当前节点前一节点,首节点或空选时返回 NULL

「把这条线请下神坛」

字典在 MQL5 里确实能漂亮地解决不少棘手的关联访问问题,比如把 OnChartEvent() 事件按对象名路由到管理类,省掉一堆手工查找。但它不是万能膏药:顺序访问用常规数组更轻,频繁增删节点时列表反而更顺手。 上面那段测试脚本把 0~9 十个整数塞进字典,再收集偶数键分两趟删除,最后打印剩余节点。作者原话是两次删除在分布式结构语义上更正确,单次删除不会带来性能收益,只是调用上可能更顺手——这点和很多人的直觉相反。 外汇与贵金属杠杆高、滑点随机,任何容器选型只影响代码效率,不改善胜率。开 MT5 把 Dictionary.mqh 挂上,跑一遍这段删除逻辑,你会看到偶数键确实清空、奇数保留,比读十篇泛泛对比更有用。

MQL5 / C++
class="type">int OnStart()
  {
   d.FreeMode(true);
   for(class="type">int i = class="num">0; i < class="num">10; ++i)
   {
     CInt *int_value = new CInt(i);
     d.AddObject(i, int_value);
   }
   class=class="str">"cmt">//-- 创建要删除的键的列表
   CArrayInt delKeys;
   for(CInt *node = d.GetFirstNode(); node != NULL; node = d.GetNextNode())
   {
     if(node.value % class="num">2 == class="num">0)
       delKeys.Add(node.value);
   }
   class=class="str">"cmt">//-- 逐个删除指定要删除的对象。
   for(class="type">int i = class="num">0; i < delKeys.Total(); i++)
     d.DeleteObjectByKey(delKeys.At(i));
   for(CInt *node = d.GetFirstNode(); node != NULL; node = d.GetNextNode())
   {
     printf(node.value);
   }
     
   class="kw">return(INIT_SUCCEEDED);
  }

常见问题

可以用字符串哈希生成整型键,配合哈希表存数据,查找接近常数时间,比线性遍历快得多。
键对应的值要做显式类型强转,或用泛型容器统一收口,否则运行期拿到的对象没法安全用。
可以,小布盯盘的 AIGC 能把你标的价位按名称建字典式记忆,开口问就能调出对应品种的标记。
装填因子超阈值(如 0.75)就要扩容重哈希,实测超了之后单次插入可能慢数倍,提前扩更稳。
模板只在编译期展开,运行期就是普通哈希访问,合理设初始桶数就不会明显拖慢实时决策。