从基础到中级:结构(二)(基础篇)
📘

从基础到中级:结构(二)(基础篇)

第 1/2 篇

「结构为何卡在两种范式之间」

在 MT5 的 MQL5 里,结构(struct)不是纯面向对象里的类,也不是早期 C 那种裸数据块,它正好卡在中间。理解这一点很关键:你既能像类一样给它挂方法,又得时刻记着它默认是值类型,赋值即拷贝。 很多交易者写 EA 时习惯把配置、品种参数塞进结构,图的是比类轻、比全局变量整洁。但一旦没吃透「值语义」,在传递行情快照结构时就会悄悄多出一份内存复制,回测里 1995 次 tick 规模下可能拖慢逻辑线程。 这个话题才刚开头,远没到能拍胸脯说「我会用结构了」的程度。后面几节会把字段对齐、方法绑定和内存布局一点点拆开。

◍ 函数里到底传结构还是传散值

MQL5 标准库已经预定义了 12 个结构,MqlDateTime 是实战里最容易碰到的之一。它本质是个 8 字节 datetime 的拆解容器:TimeToStruct 把 64 位 datetime 拆成 year、mon、day、hour、min、sec、day_of_week、day_of_year 八个 int 字段,分配好之后你就能直接读散值。 第一种写法把结构拆开,只把 day、mon、year 三个 int 喂给 DateToString。函数签名只认原语类型,第 16 行做了边界检查:day 超出 1~31 或 month 超出 1~12 就返 ERROR。代码 01 第 11 行故意传 (3,15,2024),month=15 直接被拦掉,这说明散传能在编译外层就锁死非法输入。 第二种写法把整个 MqlDateTime 通过引用传进去,函数内部看得到全部 8 个字段。功能结果和图 01 一致,但第 12 行就能肉眼看到 month 越界,调试更直白;代价是第 17 行必须按引用接结构,函数被赋予了比运行所需更多的数据特权。 别把结构当万能胶 例程知道的数据越少越好。传散值减少了声明和传递量,未来改逻辑、查 bug 都更快;传结构引用则把无关字段也暴露给函数,在高频 EA 里可能放大误改风险。外汇和贵金属杠杆高,这类隐性耦合在实盘里可能放大滑点外的逻辑错误。 MQL5 允许两种都写,但普遍适用的正解不存在——你传的比必要信息多越多,耦合越重。等后面聊类和对象时,最小知识原则会更现实,不过眼下先记住:OnStart 里 TimeLocal 取的是 MT5 所在机时钟,秒级精度,和经纪商服务器时间可能差几分钟,验证时别拿本地时间对 K 线。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
{
    class="type">MqlDateTime dt;

    TimeToStruct(TimeLocal(), dt);

    Print(DateToString(dt.day, dt.mon, dt.year));
    Print(DateToString(class="num">3, class="num">15, class="num">2024));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string DateToString(class="type">int day, class="type">int month, class="type">int year)
{
    if ((day < class="num">1) || (day > class="num">31) || (month < class="num">1) || (month > class="num">12))
        class="kw">return "ERROR...";

    class="kw">return StringFormat("%02d/%02d/%d", day, month, year);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct class="type">MqlDateTime
  {
   class="type">int year;
   class="type">int mon;
   class="type">int day;
   class="type">int hour;
   class="type">int min;
   class="type">int sec;
   class="type">int day_of_week;
   class="type">int day_of_year;
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
{
    class="type">MqlDateTime dt;

    TimeToStruct(TimeLocal(), dt);

    Print(DateToString(dt));

手动拼日期字符串的边界判断

在 MT5 里用 MqlDateTime 结构传参时,若自己写转换函数,必须先卡死日与月的合法区间。上面这段代码把 day 限制在 1–31、mon 限制在 1–12,越界直接返回 ERROR...,避免写出 00/00/2024 这类无效串。 StringFormat 用 %02d/%02d/%d 把 3、15、2024 格式化为 03/15/2024,前导零靠 %02d 补齐。实际跑这段,Print 输出就是 03/15/2024;若把 dt.day 改成 0,则返回 ERROR...。 外汇与贵金属行情里日期解析错乱会导致回测样本偏移,手动拼串虽快但容错弱,上机把 day 设成 32 验证一下返回分支就清楚了。

MQL5 / C++
class="num">11.    dt.day  = class="num">3;
class="num">12.    dt.mon  = class="num">15;
class="num">13.    dt.year = class="num">2024;
class="num">14.    Print(DateToString(dt));
class="num">15. }
class="num">16. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">17. class="type">class="kw">string DateToString(class="type">MqlDateTime &arg)
class="num">18. {
class="num">19.    if ((arg.day < class="num">1) || (arg.day > class="num">31) || (arg.mon < class="num">1) || (arg.mon > class="num">12))
class="num">20.       class="kw">return "ERROR...";
class="num">21.
class="num">22.    class="kw">return StringFormat("%02d/%02d/%d", arg.day, arg.mon, arg.year);
class="num">23. }
class="num">24. class=class="str">"cmt">//+------------------------------------------------------------------+

「把记录塞进全局结构才顺手」

结构在 MQL5 里就是一种特殊数据类型,和 uchar、double 没本质区别,都能存值、能运算。前面用零散变量或数组拼记录(代码 03、04),字段一增减就要改一大片,笨拙且易漏。把字段收进一个 struct(代码 05)思路就通了,但第 8 行那种写法把结构声明在 OnStart 内部,只能局部用,没法像代码 02 那样跨函数传数据。 一般结构要提到全局作用域(代码 06 第 4 行),这样主块外也能用。代码 06 里第 17 行建数组、21–32 行加记录,每条调用第 38 行 NewRecord 函数,它只拿最低权限参数,雇佣日期由函数内第 44 行取当前日期填,返回完整结构。第 49 行函数则演示改完结构再返回,调用方决定落点。跑出来图 03 就是一张整齐记录表。 代码 07 换了个做法:第 14 行搞了个全局变量,22–32 行插记录,其中 26–32 行手动干的事跟第 41 行函数一样,纯手写风险高,错一处能查几小时。最绕的是第 92 行函数:94 行临时结构、96 行清内存、98 行循环比对 ID,101 行判是否打印,110 行显式转 bool 告诉调用方找没找到,供第 37 行循环停。外汇/贵金属 EA 里这么传数据,作用域乱了可能出隐性 bug,MT5 实测前别盲信。 对比看,代码 06 全局结构 + 函数返回,比代码 07 手搓更不易错。下篇要聊的面向对象过渡,根基就在这层作用域挪动上。

MQL5 / C++
<span class="number">class="num">01</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">02</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span>
<span class="number">class="num">03</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">04</span>. <span class="keyword">class="type">void</span> <span class="functions">OnStart</span>(<span class="keyword">class="type">void</span>)
<span class="number">class="num">05</span>. {
<span class="number">class="num">06</span>.&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Name,
<span class="number">class="num">07</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Surname,
<span class="number">class="num">08</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Address,
<span class="number">class="num">09</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Position;
<span class="number">class="num">10</span>.&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Remuneration;
<span class="number">class="num">11</span>.&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">class="kw">datetime</span>&nbsp;&nbsp;&nbsp;&nbsp;Birth,
<span class="number">class="num">12</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Hiring;
<span class="number">class="num">13</span>.
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; .

常见问题

函数只改局部计算且数据小就传散值;要跨函数保持状态或修改字段就传结构引用,避免来回拷贝丢数据。
个位月日必须补零成两位,跨年跨月边界先算真实日历差再拼,别用固定长度截取,否则会错位。
小布可以读你的品种页逻辑,自动标出传值/传引用混用和日期拼接越界,直接给修正建议。
全局结构一次定义全程可读写,免去了多层函数签名传参,记录累积和回看都更直接。
大概率是。先定好结构归属(全局持有还是调用方持有),范式摇摆就会消失,代码也短一截。