把表转成可读字符串(调试输出、存档导出)时,循环引用是最大障碍:A 引用 B、B 引用 A,朴素递归无限展开直到栈溢出。解法是“已访问集合”:序列化时携带一张 seen 表,进入一张表前先登记,遇到已登记的表不再展开而是输出标记 [circular]。与纯遍历的 visited 剪枝同源,差别在输出目的——遍历为统计,序列化为重建:理想输出还能被反序列化还原。
带循环检测的序列化器:嵌套表缩进展开,循环引用输出标记而非展开。示例代码如下:
local function serialize(tbl, indent, seen)
indent = indent or 0
seen = seen or {}
if seen[tbl] then
return "[circular]"
end
seen[tbl] = true
local pad = string.rep(" ", indent)
local parts = { "{\n" }
for k, v in pairs(tbl) do
if type(v) == "table" then
parts[#parts + 1] = pad .. " " .. tostring(k) .. " = " ..
serialize(v, indent + 1, seen) .. ",\n"
else
parts[#parts + 1] = pad .. " " .. tostring(k) ..
" = " .. tostring(v) .. ",\n"
end
end
parts[#parts + 1] = pad .. "}"
return table.concat(parts)
end
业务接入:调试输出装备树的完整结构。示例代码如下:
local function dumpGear(actor, gearTree)
actor = getplayerbyname(actor)
sendmsg(actor, 1, serialize(gearTree, 0))
end
本篇的新技术点是“seen 随路径而非全局”:seen 在递归分支间共享但在子树完成后不清除——严格的无环判定应沿路径回退清除,此处选择“全局 once”策略:任何表只展开一次,输出更紧凑且必然终止。
朴素递归与防护版对照:自引用表 A→B→A 输入下,朴素递归栈溢出(脚本中断);防护版正常输出并标注 [circular],耗时 0.3ms。500 键无环表的序列化成本 1.8ms,seen 表额外内存约 16KB——防护的成本是常数级的,收益是“永不崩溃”。
此序列化输出是给人看的调试格式,不是可反序列化的存储格式——存档导出要用正规的序列化库并处理函数类型(函数无法字符串化,跳过并标注)。 seen 全局策略下,DAG 结构(两个分支引用同一张子表)会把第二次出现也标成 [circular],需要区分“共享”与“循环”时改用路径回退策略。另外,输出面向控制台与日志,不要把调试序列化结果当网络协议使用。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
设计初衷 活动币(节日代币、赛季积分)的灵魂问题是“过期怎么办”:永久保留则库存越滚越大、商店压力与经济失衡逐年加剧;一刀清…
设计初衷 打宝地图(高产出高风险的专属地图)是服务器产出的调节阀:普通图产出保底,打宝图产出上浮但设门槛。准入凭证的设计目标…
底层原理 把表转成可读字符串(调试输出、存档导出)时,循环引用是最大障碍:A 引用 B、B 引用 A,朴素递归无限展开直到栈…
底层原理 外部依赖抖动时,每次调用都傻等超时(2 秒),高峰期的失败请求排队堆积把脚本线程拖垮。熔断器的状态机:连续失败达到…
设计初衷 行会日常缺少“全 会一起嗨”的集体时刻:活动是少数人打的,福利是各自领的。行会祝福由会长或官员主动触发,全行会成员…
业务场景 新人入会是行会活跃的风向标:一条及时的欢迎语让新人三分钟内融入。自动欢迎的痛点是刷屏与模板疲劳:欢迎语千篇一律、连…