【语法算法】
先抛一个坑:删掉的一条数据,去哪了?听上去是句废话——删了就是没了。可账本一多就知道没那么简单:遍历到一半把成员从表里抠掉,下一项直接跳过去被漏掉;每删一条把后面的全部前移,删十次搬一千次。懒删除的答案是:删,先不做——盖一个"没了"的记号,查找时跳过它,攒到记号过半,一口气压实清扫。这篇把整套写法拆开:配置表、记号删除、跳过遍历、压实清扫、定时器、调试按钮,一段一段照抄能跑。
一、效果演示:先做记号的延迟清扫
演示场十格账本。点「删除一条」:随机一条数据当场打上黄叉——格子还在、数据还在,只是多了个记号,"记号"的小字挂在格子角上。连删几条,黄叉越攒越多,存活数往下掉。点「压实」:活的数据哗啦一声挤到前排,黄叉连着空格一起清掉——真删发生在这一拍——记号期的账全部结清,账本恢复到满员也干净的两头都硬。演示里试两笔账:记号期的一次遍历跳过黄叉照常走完,和立刻删除时遍历到一半表长变了的错乱——懒的那一拍,正是遍历最安全的那一拍。懒删除教的是延迟:不是不删,是把删挑个不疼的时候——遍历中的安全,就是这一延买来的。
flowchart TD
A[删除请求] --> B[盖记号 不搬移]
B --> C[遍历时跳过记号]
C --> D{记号过半了吗}
D -- 否 --> C
D -- 是 --> E[压实 活数据前移 记号清空]
E --> C
fx-lazydel
二、底层原理:一次记号删除加一次压实清扫
模块的机关是一对搭档。记号删除:删除请求只改一个布尔——数据原地不动,遍历的表长不变,正在进行的循环不会被删穿。压实清扫:记号攒到过半的那一拍把活数据整体前移——搬移一次付清,记号全部清空,账本恢复满员的干净。和立刻删除的分野在"懒删除把搬移的账攒着付":立刻删是每删一次搬一次尾,删得勤搬得也勤;懒删是删十次搬一次,把搬移的开销从频率里拿出来,攒成一次性付清。
压实为什么等过半才做?删一条压一次的版本压实变成常态搬移,懒字白写;全满才压的版本记号堆成山,遍历净跳空格白耗——过半是一条"跳的成本快追上搬的成本"的平衡线,到了就收账——收早了懒名不副实,收晚了账本里全是记号。
三、核心代码:完整模块(上·骨架)
-- @file LazyDelete.lua
-- 懒删除 —— 先做记号的延迟清扫
local LazyDelete = {}
local CONST = {
SLOT_CAP = 64,
COMPACT_HALF = true,
AUTOINC_BASE = 1145000,
}
local _slots = {}
local _count = 0
local _tombN = 0
local function ShowTip(msg)
if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end
function LazyDelete.Count()
return _count - _tombN
end
function LazyDelete.TombCount()
return _tombN
end
function LazyDelete.Add(value)
if _count >= CONST.SLOT_CAP then
LazyDelete.Compact()
if _count >= CONST.SLOT_CAP then
ShowTip("账本真满了——压实也救不回")
return nil
end
end
_count = _count + 1
_slots[_count] = { value = value, tomb = false }
return _count
end
四、核心代码:完整模块(下·记号删除与压实清扫)
-- 记号删除:只盖记号不搬移
function LazyDelete.Remove(idx)
local slot = _slots[idx]
if not slot or slot.tomb then return false end
slot.tomb = true
_tombN = _tombN + 1
if CONST.COMPACT_HALF
and _tombN * 2 >= LazyDelete.Count() then
LazyDelete.Compact()
end
return true
end
-- 遍历:跳过记号,遍历中删除安全
function LazyDelete.Each(func)
for i = 1, _count do
local slot = _slots[i]
if not slot.tomb then
if func(slot.value, i) == false then
break
end
end
end
end
-- 压实清扫:活数据前移,记号连格清空
function LazyDelete.Compact()
local write = 0
for read = 1, _count do
local slot = _slots[read]
if not slot.tomb then
write = write + 1
if write ~= read then
_slots[write] = slot
_slots[read] = nil
end
else
_slots[read] = nil
end
end
_count = write
_tombN = 0
ShowTip("压实了——真删发生在这一拍")
end
SL:ScheduleOnce(function()
SL:BindDebugButton("演示增删", function()
LazyDelete.Add("账" .. math.random(100, 999))
end)
ShowTip("技能已加载: 懒删除 (调试按钮触发)")
end, 1.0)
function LazyDelete.Unload()
_slots = {}
_count = 0
_tombN = 0
end
return LazyDelete
五、机制问答
问:遍历中调删除真的安全吗?
答:安全——记号删除不动表长不搬位,遍历的游标照常走,删的只是内容的心跳。
问:查找时谁被跳过?
答:盖记号的——遍历的口子里记号成员直接略过,查找的逻辑不用自己判,账本替它判了。
问:压实挑什么时候做?
答:过半即压是默认——也可挂定时压或停服压,原则只有一条:压实的搬移别赶上战斗的峰值。
问:压实期间新加的成员会丢吗?
答:不会——新增走尾部追加,压实的游标从头部往尾部写,写完尾部的新成员原地待着,两头互不打扰。
问:记号和删除标记有分别吗?
答:没分别——记号就是删除标记的口头叫法,重点不在叫什么,在"删了但没搬"这四个字——搬不搬,是懒和勤的分界线。
六、调参与实战怎么用
第四笔是压实的时机:过半即压是跳表成本和搬移成本的平衡线——全满才压的版本遍历净跳空格,删一压一的版本搬移成了常态,过半刚好卡在两头都不疼的位置。第三笔是遍历的豁口:遍历的跳过是记号的消费端——业务遍历一律走遍历口子不裸拿下标,裸拿下标的代码会一头撞上记号格。第二笔是新增的通路:尾部追加是压实的邻居——压实从头部写起、新增往尾部挂,两头的游标互不相见,这也是记号期能照常进货的原因——进货不停摆,压实才敢随时做。第一笔是计数的两本:总数和记号数分开记——存活数等于两数相减,混记一本的版本压实前后对不上账。常见坑三个:压实期间新增的成员被游标覆盖;遍历裸拿下标撞上记号格;压实后外部存的下标全部失效没人通知。
实战里懒删除是"边遍历边删"的通用答案:掉线成员的清理、过期加持的扫除、飘字到期的回收,全是同一套记号加压实的骨架。组里的约定是:凡是删除发生在遍历中的账,一律懒删不硬删——遍历的安全比删得利索值钱。这篇的模块照抄能跑,改的就是 CONST 那张表。
写完留一句给做账本系的同学:懒删除卖的是"删也要挑时候"——那一格一格的黄叉记号和压实一拍的整体前移,是把删除的开销从频率里摘出来的一次延迟清账,清得体面。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…
【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…
【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…
【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…
【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…
【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…