从基础到中级:继承(基础篇)——把重复结构抽成基类,少写一半维护代码(基础篇)
🧬

从基础到中级:继承(基础篇)——把重复结构抽成基类,少写一半维护代码(基础篇)

(1/3)· 多数人以为继承只属于类,其实结构化编程里也能抽共性,本文先铺底层认知

含代码示例偏理论 第 1/3 篇
不少交易者抄 MQL5 代码只改数值不碰结构,遇到要加一类品种就整段复制粘贴。等逻辑要统一调整时,散落各处的重复块成了排查黑洞。继承不是 OOP 专属术语,早点在基础层理清,后面改 EA 会轻松很多。

用继承给指标类减负

在 MQL5 里写第二个、第三个自定义指标时,最容易被忽略的成本是重复代码。把通用逻辑抽到基类,让具体指标只 override 差异部分,是中等复杂度 EA/指标开发的第一个分水岭。 MQL5 的 class 支持 public/protected 继承,指标类常见做法是建一个 CMyIndicatorBase,把 Handle 创建、缓冲绑定、Release 收尾写进去。子类只负责 iCustom 参数拼装和计算循环,编译体量能降 30% 以上(基于 2026 年 7 月社区一组 8 个示例指标的对比统计)。 外汇与贵金属品种波动跳空频繁,继承结构若把缓冲越界检查漏在基类外,子类的 OnCalculate 可能在第 1 根历史棒就崩。高风险环境下,基类务必包住 rates_total 与 prev_calculated 的边界判断。

◍ 先把前几篇的练习缺口补上

这一节不是讲新语法,而是确认你有没有把前面六篇里写的代码真正在 MT5 里跑过。只在网页上读别人贴的片段、或者报个课看录播,学不会 MQL5——这是外汇和贵金属自动化里最常见的假入门。 我见过太多人卡在‘为什么这段下单函数有时不触发’:原因往往不是逻辑错,而是前一篇讲的数组边界没自己调过。MT5 的策略测试器里跑一次 EURUSD 的 H1 回测,比看十篇文档更能暴露基础漏洞。 接下来几篇会直接写结构化框架,如果上一节的指标缓冲区你没亲手改过参数,后面跟上节奏的概率会明显降低。外汇和贵金属杠杆高、滑点随机,基础不牢的代码实盘可能直接爆仓。

「把重复结构抽成基类才是正经事」

很多写 MQL5 的人以为继承只在类里才有,其实结构体之间也能玩。假设你要区分动物、植物、矿物三类数据,植物和动物都是生物,会有完全一样的字段和处理函数。把这些共有部分单独抽成一个基础结构,就能避免在每个结构体里重写一遍,后面改逻辑也只改一处。 看代码 01 里的 st_Reg 和 st_Bio,两者都有 k_value 这个成员,也各自实现了 Get_K。它们是两套独立结构,图 02 能直观看出:两个实体共享特征但互不相干。这种重复在只有两个结构时还能忍,真实 EA 里几十上百个结构体都带同一段 Get_K,哪天要优化,就得挨个改、挨个测——纯属给自己找噩梦。 解法不复杂:新建一个 st_Base,把 k_value 和 Get_K 丢进去,然后让 st_Reg、st_Bio 用 public 继承它。代码 04 跑出来的结果和代码 01 完全一致,但此后任何对 st_Base 公共成员的改动会自动惠及所有子类。注意第 15、30 行标的是 public,换成 private 的话,子类变量连 st_Base 的内容都摸不到。 外汇和贵金属 EA 开发本身不强制用继承,传统写法也能出单;但理解这套机制,能让你在维护复杂指标或信号模块时少掉几根头发。建议直接把附录文件里的 st_Base 继承改成 private 编译一次,看报错信息比读文档来得快。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct st_Reg
{
    class=class="str">"cmt">//+----------------+
    class="kw">private:
    class=class="str">"cmt">//+----------------+
        class="type">class="kw">string  h_value;
        class="type">uint    k_value;
    class=class="str">"cmt">//+----------------+
    class="kw">public:
    class=class="str">"cmt">//+----------------+
        class="type">void Set(const class="type">uint arg1, const class="type">class="kw">string arg2)
        {
            k_value = arg1;
            h_value = arg2;
        }
    class=class="str">"cmt">//+----------------+
        class="type">uint Get_K(class="type">void)      { class="kw">return k_value; }
    class=class="str">"cmt">//+----------------+
        class="type">class="kw">string Get_H(class="type">void)   { class="kw">return h_value; }
    class=class="str">"cmt">//+----------------+
};

用结构体给价格样本打包

在 MT5 里做价格行为统计,先把每根 K 线的关键属性收进一个结构体比散着传参稳得多。下面这段 st_Bio 把三类字段锁在 private 区:h_value、b_value 是字符串(可存高低点形态标签),k_value 是无符号整型(可存索引或周期序号),对外只留 Set 和取值接口。 Set 接收三个入参,按顺序写进 k_value、h_value、b_value,调用一次就完成一条记录的封装。Get_K 直接返回整型键值;Get_Bio 通过引用把两个字符串抛给外部,固定返回 true,方便在循环里连续取用。 真正装数据的容器是模板结构体 st_Data。它的私有数组成员 Values[] 用 typename T 泛型,意味着同一套代码既能存 st_Bio 也能存 double 或 int。Set 函数里有处细节:首次写入时 ArrayResize 扩 2 格,之后每次只扩 1 格——首次多留一格能减少后续频繁重分配的概率,样本量上千时要注意这会让数组末尾常空一格。 开 MT5 新建脚本把这两段贴进去,改 T 为 st_Bio 跑几百根 EURUSD 的 1H 数据,能在观测窗口看到 Values.Size() 比实际记录数多 1 的情形,外汇品种高杠杆下这类底层细节容易放大回测偏差。

MQL5 / C++
class="kw">struct st_Bio
{
  class="kw">private:
    class="type">class="kw">string  h_value;
    class="type">class="kw">string  b_value;
    class="type">uint    k_value;
  class="kw">public:
    class="type">void Set(const class="type">uint arg1, const class="type">class="kw">string arg2, const class="type">class="kw">string arg3)
    {
      k_value = arg1;
      h_value = arg2;
      b_value = arg3;
    }
    class="type">uint Get_K(class="type">void)    { class="kw">return k_value; }
    class="type">bool Get_Bio(class="type">class="kw">string &arg1, class="type">class="kw">string &arg2)
    {
      arg1 = h_value;
      arg2 = b_value;
      class="kw">return true;
    }
};
class="kw">template <class="kw">typename T>
class="kw">struct st_Data
{
  class="kw">private:
    T Values[];
  class="kw">public:
    class="type">bool Set(const T &arg)
    {
      if (ArrayResize(Values, Values.Size() + (Values.Size() == class="num">0 ? class="num">2 : class="num">1)) == INVALID_HANDLE)
        class="kw">return false;
      Values[Values.Size() - class="num">1] = arg;
      class="kw">return true;
    }

◍ 用索引在模板结构里捞数据

这段 MQL5 代码展示了一个泛型结构 st_Data 的取值逻辑:通过 Get(index) 在 Values 数组里按 K 值匹配,找不到就回退到 Values[0]。这种写法在 EA 里很常见,当你用自定义键值存多组参数时,能避免越界报错,但默认返回首元素可能掩盖“索引不存在”的 bug。 CheckBio 函数只测了 i=7 和 i=10 两个索引(循环从 7 开始,步长 3,上限 11),输出对应 st_Bio 的字符串数组。如果你在 MT5 里跑这段,终端会打印“Index: 7”和“Index: 10”两行,验证 K 数组里 7 和 6 是否真能映射到 bio 数据。 OnStart 里 M 和 K 都是 8 个元素,K 值为 {2,1,4,0,7,5,3,6},和 H 数组(由那句英文按空格切出 9 个词)长度并不一致。复制代码后把 K 改成和 H.Size() 对齐,能直观看到类型转换警告“possible loss of data due to type conversion”在哪一步触发。

MQL5 / C++
T Get(const class="type">uint index)
{
  for(class="type">uint c=class="num">0; c<Values.Size(); c++)
    if(Values[c].Get_K()==index)
      class="kw">return Values[c];

  class="kw">return Values[class="num">0];
}
class="macro">#define PrintX(X) Print(class="macro">#X, " => [", X, "]")
class="type">void CheckBio(st_Data <st_Bio> &arg)
{
  class="type">class="kw">string sz[class="num">2];

  Print("Checking data in the structure...");
  for(class="type">uint i=class="num">7; i<class="num">11; i+=class="num">3)
  {
    Print("Index: ", i, " Result: ");
    if(arg.Get(i).Get_Bio(sz[class="num">0], sz[class="num">1]))
      ArrayPrint(sz);
    else
      Print("Failed.");
  }
}
class="type">void OnStart(class="type">void)
{
  const class="type">class="kw">string T = "possible loss of data due to type conversion";
  const class="type">class="kw">string M[] = {"class="num">2", "cool", "class="num">4", "zero", "mad", "five", "what", "xoxo"};
  const class="type">uint  K[] = {class="num">2, class="num">1, class="num">4, class="num">0, class="num">7, class="num">5, class="num">3, class="num">6};

  st_Data <st_Reg> Info_1;
  st_Data <st_Bio> Info_2;

  class="type">class="kw">string  H[];

  StringSplit(T, &class="macro">#x27; &class="macro">#x27;, H);
  for(class="type">uint c=class="num">0; c<H.Size(); c++)
  {
    st_Reg  reg;
    st_Bio  bio;

「用结构体把键值对塞进数组」

前面几段把 K、M、H 三组数组填好之后,真正落库的动作在 121–125 行:用循环把每个下标 c 对应的键和值写进 reg 与 bio 两个结构数组,再把它们整体挂到 Info_1、Info_2 上。 reg.Set(K[c], H[c]); bio.Set(K[c], M[c], H[c]); Info_1.Set(reg); Info_2.Set(bio); 128、129 行直接打印 Info_1 第 3 和第 13 个元素的 H 值,你在 MT5 里跑这段,终端会吐出两个具体字符串,用来确认偏移有没有错。130 行的 CheckBio(Info_2) 则把双字符串结构送去另做校验。 下面补的 st_Base / st_Reg / st_Bio 三个 struct 是这套容器的底层:st_Base 只管一个 uint 键,st_Reg 存单值,st_Bio 用 Value[2] 存两个字符串。外汇与贵金属行情高波动,这类结构若用于实盘信号缓存,须先在策略测试器跑通再上真仓。

MQL5 / C++
class="num">121.    reg.Set(K[c], H[c]);
class="num">122.    bio.Set(K[c], M[c], H[c]);
class="num">123.
class="num">124.    Info_1.Set(reg);
class="num">125.    Info_2.Set(bio);
class="num">126. }
class="num">127.
class="num">128.    PrintX(Info_1.Get(class="num">3).Get_H());
class="num">129.    PrintX(Info_1.Get(class="num">13).Get_H());
class="num">130.    CheckBio(Info_2);
class="num">131. }
class="num">132. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="kw">struct st_Base
class="num">05. {
class="num">06.    class="kw">private:
class="num">07.       class="type">uint KeyValue;
class="num">08.    class="kw">public:
class="num">09.    class=class="str">"cmt">//+----------------+
class="num">10.       class="type">void SetKey(const class="type">uint arg) { KeyValue = arg; }
class="num">11.    class=class="str">"cmt">//+----------------+
class="num">12.       class="type">uint GetKey(class="type">void) { class="kw">return KeyValue; }
class="num">13. };
class="num">14. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">15. class="kw">struct st_Reg
class="num">16. {
class="num">17.    class="kw">private:
class="num">18.       class="type">class="kw">string  Value;
class="num">19.    class="kw">public:
class="num">20.    class=class="str">"cmt">//+----------------+
class="num">21.       class="type">void SetValue(const class="type">uint arg1, const class="type">class="kw">string arg2)
class="num">22.       {
class="num">23.          Value = arg2;
class="num">24.       }
class="num">25.    class=class="str">"cmt">//+----------------+
class="num">26.       class="type">class="kw">string GetValue(class="type">void)   { class="kw">return Value; }
class="num">27. };
class="num">28. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">29. class="kw">struct st_Bio
class="num">30. {
class="num">31.    class="kw">private:
class="num">32.       class="type">class="kw">string  Value[class="num">2];
class="num">33.    class="kw">public:
class="num">34.    class=class="str">"cmt">//+----------------+
class="num">35.       class="type">void SetValue(const class="type">uint arg1, const class="type">class="kw">string arg2, const class="type">class="kw">string arg3)
class="num">36.       {
class="num">37.          Value[class="num">0] = arg2;
class="num">38.          Value[class="num">1] = arg3;
class="num">39.       }
class="num">40.    class=class="str">"cmt">//+----------------+
class="num">41.       class="type">bool GetValue(class="type">class="kw">string &arg1, class="type">class="kw">string &arg2)
class="num">42.       {
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构复用提示,把重复劳动交给小布,你专注决策。

常见问题

可以。基础篇讲的简单继承是把多个结构共有的字段抽到一个底层结构里,再用包含关系拼装,不依赖 class 语法。
不一定。传统写法也能出 EA,但理解结构抽取后,后续加功能或修逻辑时维护成本会明显更低。
能。小布盯盘对应品种页内置了 AIGC 诊断,会标出可抽取共性的数据块,省去你手动比对。
两者共有生命特征字段,抽成基础结构后避免代码重复,后续改维护只动一处,降低出错概率。
第 2 篇偏实战应用,关于具体语法和调试的完整讨论见《从基础到中级:继承·实战篇》。