【语法算法】
一、一行代码的代价
table.remove(t, 1)——弹出队头最顺手的一行。Lua 5.1 里它的实现是数组段整体前移:第 2 到第 n 个元素逐格左移,末位置 nil。队里一千个元素,弹一次搬九百九十九次;战斗飘字、伤害队列、拾物事件每帧都在弹头,等于每帧做一次微型搬家。问题的根子不在 table.remove 写得差,而在数组段的物理形态:连续、有序、下标即身份,物理弹头等于整队平移。
二、底层原理:两根指针夹出活动区间
零搬移的思路是"不删元素,只动边界"。队列维护两根指针:_index 是头指针,_border 是尾指针。push 把元素写进 _data[_border],_border 加一;pop 取出 _data[_index] 并置 nil,_index 加一。任意时刻活动元素都夹在两指针之间,size() 是一次减法,push 与 pop 双向 O(1)。数组前段会留下已消费的空洞,这是设计内的代价——队列耗尽时一次性 reset 重建表壳,把带洞的旧表整体丢给 GC。
flowchart LR
subgraph data["_data 数组段"]
d0["[0] nil"] --- d1["[1] nil"] --- d2["[2] 弹出中"] --- d3["[3] 活动"] --- d4["[4] 活动"]
end
I["头指针 _index = 2"] -.-> d2
B["尾指针 _border = 5"] -.-> d4
P["push:写 _data[_border],_border+1"] --> d3
O["pop:取 _data[_index] 置 nil,_index+1"] --> d2
三、正确写法(可直接落地)
local queue = class('queue')
function queue:ctor()
self:reset()
end
function queue:push(d)
self._data[self._border] = d
self._border = self._border + 1
end
function queue:pop()
local ret = nil
if self._index < self._border then
ret = self._data[self._index]
self._data[self._index] = nil -- 断引用,交给 GC
self._index = self._index + 1
else
self:reset() -- 队列耗尽,重建表壳
end
return ret
end
function queue:front()
if self._index < self._border then
return self._data[self._index]
end
return nil
end
function queue:size()
return self._border - self._index
end
function queue:empty()
return self:size() == 0
end
function queue:reset()
self._index = 0
self._border = 0
self._data = {}
end
四、三个必须钉死的细节
五、老手再看一眼
热路径的最后一步:把 queue 实例与常用方法提为局部变量,table.insert 提成 upvalue,循环里省掉全局查找与点号寻址。对象池内部拿这条队列做回收通道,每帧进出几百个节点也不产生一次搬移。四十行代码换来帧循环里确定性的 O(1)——数据结构选型在游戏前端最直接的回报,就长这样。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 next 的遍历本质就这一行: 传入上一个键,返回下一个键值对——不传上一个键就从第一个开始——next 是 …
【语法算法】 返回值的截取本质就这一行: 函数返回三个值,左侧三个变量各接一个——多余的返回值被丢弃,不足的补 nil——多…
【语法算法】 collectgarbage 的分步回收就这一行: "step" 模式让 GC 执行一步增量回收——参数 20…
【语法算法】 gmatch 的迭代本质就这一行: gmatch 返回一个迭代函数——每次调用返回下一个匹配——遍历完返回 n…
【语法算法】 xpcall 的错误处理函数就这一行: xpcall 和 pcall 的本质区别就一个:xpcall 可以传入…
【语法算法】 元方法 __index 设为函数的本质就这一行: 读取表中不存在的键时,Lua 调用 __index 函数——…