3.表拷贝与快照
3.1 知识点
为什么要聊拷贝
上一篇说了表引用:local b = a 只是多一个入口,不会拷贝。
拷贝只在要隔离数据时才有价值,比如战斗快照、回滚、UI 临时展示。深拷贝看着最干净,但成本也是最高的。拷贝前先想清楚:
- 只要外层独立,还是内层表也要独立?
- 后面会不会被改?是不是存某一时刻的状态?
- 是真需要拷贝,还是怕别人乱改?
浅拷贝
浅拷贝只复制第一层字段。
外层表是新的,内层 table 还是原来的引用。
local function ShallowCopy(source)
local copy = {}
for key, value in pairs(source) do
copy[key] = value
end
return copy
end
例子:
local oldData = {
id = 1,
name = "Player",
attr = {
hp = 100,
mp = 50
}
}
local newData = ShallowCopy(oldData)
newData.name = "NewPlayer"
newData.attr.hp = 80
print(oldData.name) -- 输出:Player
print(oldData.attr.hp) -- 输出:80
name 在第一层,所以分开了。attr 是内层表,仍然共享。
浅拷贝适合数据结构很浅、内层数据本来就允许共享的情况。
如果后面要改内层表,浅拷贝是挡不住污染的。
深拷贝
深拷贝要处理的不只是“递归复制字段”:
- 子表也要复制。子表的子表也要。
- 同一张旧表在被拷贝表中引用多次,新表里也应该保持共享关系。
- 循环引用不能递归到栈溢出。
- 非
table的值一般直接保留。
local function DeepCopy(object, copied, keepMetatable)
if type(object) ~= "table" then
return object
end
copied = copied or {}
if copied[object] then
return copied[object]
end
local copy = {}
-- 先登记,再递归填字段;self 引用和重复引用都靠这张表兜住。
copied[object] = copy
for key, value in pairs(object) do
copy[DeepCopy(key, copied, keepMetatable)] = DeepCopy(value, copied, keepMetatable)
end
if keepMetatable then
setmetatable(copy, getmetatable(object))
end
return copy
end
copied 缓存已经拷贝的对象,避免循环引用栈溢出,重复引用也会指向同一个复制后的对象。
以上代码只处理了普通 Lua 数据。function、userdata、thread 会原样返回。
元表除非调用方明确传 keepMetatable = true,否则默认不带。
循环引用和重复引用
循环引用是表里又指回了自己,如果没有 copied 记录已经拷贝过的表,递归会一直往下走,最后栈溢出。
local node = {
name = "Node"
}
node.self = node
local copyNode = DeepCopy(node)
print(copyNode ~= node) -- 输出:true,新旧 node 不是同一张表
print(copyNode.self == copyNode) -- 输出:true,self 指向拷贝后的 node 自己
关键点是:拷贝 node 时要先创建 copyNode,然后立刻记录:
copied[node] = copyNode
后面再拷到 node.self,发现 node 已经拷过了,就直接返回 copyNode,就不会继续递归下去。
重复引用是多个字段引用同一张子表。
local sharedAttr = {
hp = 100
}
local data = {
attrA = sharedAttr,
attrB = sharedAttr
}
local copyData = DeepCopy(data)
print(data.attrA == data.attrB) -- 输出:true,原数据里 attrA 和 attrB 共用同一张 attr
print(copyData.attrA == copyData.attrB) -- 输出:true,拷贝后仍然保持共用关系
print(copyData.attrA ~= sharedAttr) -- 输出:true,拷贝后的 attr 已经和旧 attr 分开
如果没有 copied,attrA 会拷一份,attrB 又拷一份。
虽然看起来也是深拷贝,但破坏了原来的共享关系。
所以 copied 不是只用来防循环引用。
它真正做的是维护一张映射表:旧对象 -> 新对象
-- 旧表 -> 新表
copied[oldTable] = newTable
同一张旧表,不管后面遇到多少次,都只会对应同一张新表。
key 也要拷贝
Lua 的 table 不只是能当 value,也可以当 key。
local key = { id = 1001 }
local data = {
[key] = "SkillData"
}
大部分表都是字符串 key、数字 key,比如 id、name、1001 这种。
但在一些映射关系里,table 当 key 也是有可能的。
比如用对象表做索引、记录某个对象对应的数据、做缓存映射,或者一些弱表结构。
key 如果也是 table,也要走同一套拷贝逻辑:
copy[DeepCopy(key, copied)] = DeepCopy(value, copied)
如果 value 拷了,但 table key 还指向旧表,那这份数据没有完全和旧数据斩断,还是藕断丝连。
元表要不要保留
一般来说元表不会去深拷贝。
很多元表代表的是类方法、代理逻辑、只读保护、懒加载规则。
按业务决定复用元表,一般不把元表再拷一份。
-- 不带元表:拷贝出来就是普通表
local copy = DeepCopy(source)
-- 复用元表:保留 __index 等行为
local copyWithMeta = DeepCopy(source, nil, true)
一般这样判断是否保留元表:
- 纯数据快照:通常不带元表。
- 对象实例拷贝:如果还要保留方法查找,可能要复用元表。
- 代理表、只读表、带
__metatable保护的表:按项目规则单独写。
function 和 userdata 怎么处理
function 一般原样保留。
函数可能带闭包和 upvalue,普通深拷贝不负责复制这些状态。
userdata 也一般原样保留。
Unity / xLua / toLua 场景里,userdata 背后可能是 C# 对象、资源句柄、Native 对象。Lua 层复制不出这样的真实宿主对象。
thread 同理,通常不尝试复制。
只有 table 往下展开。
其他类型直接返回。
if type(object) ~= "table" then
return object
end
快照
快照的意思是把某一刻的数据存下来。可以用深拷贝。
local snapshot = DeepCopy(player)
适合做快照的东西:
- 战斗结算前的角色属性。
- 回放系统某一帧的状态。
- 调试时保存现场数据。
- UI 展示用临时数据。
不适合随手快照的东西:
- 全局配置表。配置应该只读共享,怕误改就做保护。
- 模块单例状态。多处引用同一份模块表,快照只会制造另一份副本。
- 正在被多个系统共享的运行时对象。拷贝后状态分叉,查问题更麻烦。
- 绑了宿主对象的数据。Lua 表复制了,
userdata还是同一个。
总结
浅拷贝只拆第一层;
深拷贝靠 copied 缓存避免循环引用和重复引用;
table 当 key 也要拷贝;
元表按业务决定是否复用;function / userdata / thread 不拷贝;
快照适合纯 Lua 数据现场。
3.2 知识点代码
Lesson3_表拷贝与快照.lua
print("**********表拷贝与快照************")
print("**********知识点一 为什么要聊拷贝************")
local role = {
id = 1,
attr = {
hp = 100
}
}
local viewData = role
viewData.attr.hp = 80
print(role.attr.hp) -- 输出:80,viewData 不是副本
print(viewData.attr.hp) -- 输出:80,两边看到的是同一张 attr 表
print("**********知识点二 浅拷贝************")
local function ShallowCopy(source)
local copy = {}
for key, value in pairs(source) do
copy[key] = value
end
return copy
end
local oldData = {
id = 1,
name = "Player",
attr = {
hp = 100,
mp = 50
}
}
local newData = ShallowCopy(oldData)
newData.name = "NewPlayer"
newData.attr.hp = 80
print(oldData.name) -- 输出:Player,第一层字段已经分开
print(newData.name) -- 输出:NewPlayer
print(oldData.attr.hp) -- 输出:80,内层 attr 仍然共享
print(newData.attr.hp) -- 输出:80
print("**********知识点三 深拷贝************")
local function DeepCopy(object, copied, keepMetatable)
if type(object) ~= "table" then
return object
end
copied = copied or {}
if copied[object] then
return copied[object]
end
local copy = {}
-- 先登记旧表 -> 新表,循环引用和重复引用都靠它处理。
copied[object] = copy
for key, value in pairs(object) do
copy[DeepCopy(key, copied, keepMetatable)] = DeepCopy(value, copied, keepMetatable)
end
if keepMetatable then
setmetatable(copy, getmetatable(object))
end
return copy
end
local sourceData = {
id = 1,
attr = {
hp = 100,
mp = 50
}
}
local copyData = DeepCopy(sourceData)
copyData.attr.hp = 60
print(sourceData.attr.hp) -- 输出:100,内层 attr 已经分开
print(copyData.attr.hp) -- 输出:60
print("**********知识点四 循环引用和重复引用************")
local node = {
name = "Node"
}
node.self = node
local copyNode = DeepCopy(node)
print(copyNode ~= node) -- 输出:true,新旧表不是同一张
print(copyNode.self == copyNode) -- 输出:true,自引用指向新表自己
local sharedAttr = {
hp = 100
}
local refData = {
attrA = sharedAttr,
attrB = sharedAttr
}
local refCopy = DeepCopy(refData)
print(refData.attrA == refData.attrB) -- 输出:true,原表里共享同一张 attr
print(refCopy.attrA == refCopy.attrB) -- 输出:true,新表里仍然保持共享关系
print(refCopy.attrA ~= sharedAttr) -- 输出:true,新旧内层 attr 已断开
print("**********知识点五 key 也可能是 table************")
local tableKey = {
id = 1001
}
local keyData = {
[tableKey] = "SkillData"
}
local keyCopy = DeepCopy(keyData)
local copiedKey = nil
local copiedValue = nil
for key, value in pairs(keyCopy) do
copiedKey = key
copiedValue = value
end
print(copiedKey == tableKey) -- 输出:false,key 也被拷成新表
print(copiedKey.id) -- 输出:1001
print(copiedValue) -- 输出:SkillData
print("**********知识点六 元表要不要保留************")
local meta = {
__index = {
defaultHp = 100
}
}
local hero = {
name = "Hero"
}
setmetatable(hero, meta)
local heroNoMeta = DeepCopy(hero, nil, false)
local heroWithMeta = DeepCopy(hero, nil, true)
print(heroNoMeta.defaultHp) -- 输出:nil,不保留元表就没有默认字段
print(heroWithMeta.defaultHp) -- 输出:100,复用元表后还能走 __index
print(getmetatable(heroWithMeta) == meta) -- 输出:true,复用的是同一张元表
print("**********知识点七 function 和 userdata 怎么处理************")
local function OnClick()
return "Click"
end
local uiData = {
callback = OnClick
}
local uiCopy = DeepCopy(uiData)
print(uiData.callback == uiCopy.callback) -- 输出:true,function 原样保留
print(uiCopy.callback()) -- 输出:Click
print("userdata/thread 一般按项目规则处理")
print("**********知识点八 快照边界************")
local player = {
id = 1,
hp = 100,
buffs = {
"SpeedUp"
}
}
local snapshot = DeepCopy(player)
player.hp = 50
table.insert(player.buffs, "PowerUp")
print(player.hp) -- 输出:50,当前数据继续变化
print(snapshot.hp) -- 输出:100,快照保留拷贝那一刻
print(#player.buffs) -- 输出:2
print(#snapshot.buffs) -- 输出:1
local SkillConfig = {
[1001] = {
id = 1001,
name = "FireBall",
damage = 100
}
}
local cfg = SkillConfig[1001]
local runtimeSkill = {
id = cfg.id,
name = cfg.name,
damage = cfg.damage,
level = 1
}
runtimeSkill.damage = runtimeSkill.damage + 20
print(runtimeSkill.damage) -- 输出:120,运行时数据自己变化
print(SkillConfig[1001].damage) -- 输出:100,配置仍保持原值
print("**********知识点九 总结************")
print("浅拷贝只拆第一层")
print("深拷贝用 copied 处理循环引用和重复引用")
print("function、userdata、thread 通常原样返回")
print("快照适合纯 Lua 数据现场")
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com