从基础到中级:结构(二)(基础篇)
「结构为何卡在两种范式之间」
在 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 线。
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 验证一下返回分支就清楚了。
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 手搓更不易错。下篇要聊的面向对象过渡,根基就在这层作用域挪动上。
<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>. <span class="keyword">class="type">class="kw">string</span> Name, <span class="number">class="num">07</span>. Surname, <span class="number">class="num">08</span>. Address, <span class="number">class="num">09</span>. Position; <span class="number">class="num">10</span>. <span class="keyword">class="type">class="kw">double</span> Remuneration; <span class="number">class="num">11</span>. <span class="keyword">class="type">class="kw">datetime</span> Birth, <span class="number">class="num">12</span>. Hiring; <span class="number">class="num">13</span>. .