【语法算法】
在 Lua 5.1/5.2 里写了半辈子循环的人都会撞同一堵墙:Lua 没有 continue。想跳过本轮剩下的逻辑,只能把主体包进 if 反转条件,把嵌套越垫越深。5.4 引入 goto 之后,社区收敛出一个标准替代写法——goto continue 配 ::continue:: 标签。今天拆这个惯用法的语法细节:标签放哪、为什么必须放在循环体末尾、它和 break 的作用域铁律,以及老版本的三种降级方案。
一、最小可用的 continue 模板
写法固定三件套:循环体内做过滤判断、goto continue 跳转、循环体末尾放标签 ::continue:::
for i = 1, 10 do
if i % 2 == 0 then
goto continue -- 跳过偶数本轮剩余逻辑
end
print("odd:", i)
::continue:: -- 标签必须位于语句块末尾
end
为什么标签要贴着 end 放?因为 goto 的跳转目标不能越过局部变量的声明——goto 不得跳入某个 local 的作用域之内,标签放在块尾就不会跨过任何 local 的出生点,编译器直接放行。反过来,若把标签写在中间某条 local 之前,编译直接报 "jumps into the scope of local 'x'"。这是 continue 模拟唯一的硬约束,记住"标签永远垫底"就不会踩。另外两条语法边界顺手记牢:标签名遵循标识符规则,::continue:: 里的 continue 只是普通名字不是关键字;一个块里标签不能重名,嵌套两层循环想各模拟一层 continue,就要用 ::cont_outer::、::cont_inner:: 这类区分命名。
二、嵌套循环:continue 与 break 各管各层
嵌套场景是这个惯用法真正体现价值的地方:goto continue 只跳过本层剩余语句,外层循环照常推进;而 break 严格绑定它所在的最内层循环。两者配合,可以写出"过滤 + 终止"清晰分层的扫描逻辑:
for wi, wave in ipairs(waves) do
if wave.cleared then goto cont_outer end
for _, mob in ipairs(wave.mobs) do
if mob.hp <= 0 then goto cont_inner end
if mob.isBoss then goto cont_inner end
damage(mob, 10)
if mob.hp <= 0 then break end -- 只退出内层怪物循环
::cont_inner::
end
::cont_outer::
end
注意两层标签各垫各的 end——::continue:: 属于它所在的那个块,想模拟哪一层的 continue,标签就得垫在哪一层的块尾。while 循环同样适用,但要多留一个心眼:goto 跳到块尾标签后,本轮剩余语句被跳过、循环条件照常在下一轮开头重算,语义正确;唯独不能用 goto 模拟"跳过条件判断"——条件永远会执行。还想"一次跳出多层",5.4 里直接 goto done 跳到循环体外的 ::done:: 标签,比 break 加标志位变量干净得多——这其实是 goto 在 Lua 里比 continue 场景更被推荐的用法。
flowchart TD
A[进入循环体] --> B{过滤条件成立?}
B -->|是| C["goto continue → 直达块尾标签"]
B -->|否| D[执行本轮主体逻辑]
D --> E{终止条件?}
E -->|是| F[break 退出最内层循环]
E -->|否| C
C --> G["标签 ::continue::(垫在块尾)"]
G --> H[回到循环条件, 下一轮]
F --> H2[外层循环继续推进]
三、老版本降级与迁移建议
老项目(5.1 / LuaJIT)没有 goto,惯用替代有三招。第一招条件反转:把主体包进 if not (cond) then ... end,最直白,但多层过滤时要写"且/或"复合条件,可读性随条件数下降。第二招早退函数:把循环体抽成局部函数,过滤命中就 return——缩进最浅,但每轮一次函数调用,且 LuaJIT 对含 upvalue 的闭包可能放弃内联,热点路径要谨慎。第三招状态标志:local skip = false 置位后主体包 if not skip,最啰嗦,只适合临时补丁。迁移到 5.4+ 时,goto continue 是损耗最低的写法:编译期就是一条跳转指令,没有调用开销,还比深层 if 嵌套的可读性高得多。团队规范建议只约束三条:标签统一命名 continue(分层场景加后缀)且置于块尾、禁止用 goto 向后跳转(那会把循环逻辑撕成面条)、禁止 goto 跳入另一个语句块内部。总结:Lua 的 continue 不是缺席,而是被 goto + 块尾标签 这个标准惯用法补齐——"标签垫底、只跳向前、不跨 local"三条铁律在手,它比任何模拟写法都更接近其他语言的原生 continue,也让 Lua 5.4 的循环第一次同时拥有了 continue 与多层 break 的表达力。
四、goto 的另一半:状态机与多层出口
continue 模拟只是 goto 的顺手一用,它在 Lua 5.4 里更对口的场景是状态机。把每个状态写成一段代码、状态转移写成 goto,解释器零开销直达,比"状态表 + while 大循环 + 名字查表"少一层间接:
::idle::
wait_event("tap")
if ready() then goto charge end
goto idle
::charge::
play_anim("charge")
if cancel() then goto idle end
goto release
::release::
fire()
goto idle
这个写法的关键纪律是状态只增不改向:每个标签块只允许向后跳,禁止往前跳回已执行过的状态,否则控制流图立即不可读。另一个实用场景是多层出口:双重循环里找到目标后要同时跳出两层,5.4 之前只能立标志位让外层判断,现在 goto found 直达循环体后的 ::found::,标志位变量整个消失。这两类用法与 continue 模拟合起来,构成了 goto 在 Lua 里的全部"正当职业";其余玩法——跳进块中间、跨 local 跳转——要么编译不过,要么属于团队规范明令禁止的范畴。一句话给 goto 定位:它不是用来写算法的,是用来表达非结构化控制流的最后一段结构——continue、多层 break、状态转移,这三个形态写完,其余需求都应该回到 if 与循环里去。还有一个编译器层面的细节值得知道:goto 的跳转发生在编译期定址,运行期零查找,而标签的可见范围是"整个当前语句块"——这意味着标签可以在 goto 语句之后定义(向前引用合法),这正是 continue 模拟能成立的语言学基础:过滤判断写在循环体开头,标签垫在结尾,编译器允许向后引用。向前引用合法、向后跳转被团队规范禁止、跳入 local 作用域被编译器禁止——三条边把 goto 的活动范围框得死死的,这也解释了为什么 Lua 社区对 5.4 引入 goto 的争议最终平息:它的能力边界被语法本身裁剪过,留给大家的恰好是"够用但写不出面条"的那一小块。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【游戏功能】 var ld2=Math.max(1,Math.round(d 0.25)) ——先拆这一行。连枝转移的账就在…
【游戏功能】 前阵子的投诉出得冤:玩家开着让先纹被怪的头一招打掉了半管血,找到客服问"让先让了个寂寞"。翻流水发现机制没算错…
【游戏功能】 上一版留了条只在连打两场时冒头的隐蔽报错:秋后纹第二次开的清算比第一次翻倍——头一场十二笔账清完没清零,第二场…
【游戏功能】 先抛一个坑:砍出去的刀,能收回来吗?刀光落了地、数字蹦出来、血条掉了——按理说木已成舟。可玩家心里都有过那一拍…
【游戏功能】 var fresh=(tg===M)?fr.a:fr.b ——先拆这一行。两只怪,两本"点没点过卯"的小账,出…
【游戏功能】 上个月一场差评事故:一个闪避堆得高的怪成了玩家的噩梦——十刀落空七刀,打得着的两三刀又不痛不痒,玩家在频道里骂…