Lua :有效地复制 table (深层复制)
Lua : copying a table efficiently (deep copy)
我尝试有效地制作 lua table 的副本。我编写了以下运行良好的函数 copyTable()(见下文)。但是我想我可以使用函数的 "passing by value" 机制来提高效率。我做了一些测试来探索这个机制:
function nop(x)
return x
end
function noop(x)
x={}
return x
end
function nooop(x)
x[#x+1]=4
return x
end
function copyTable(datatable)
local tblRes={}
if type(datatable)=="table" then
for k,v in pairs(datatable) do tblRes[k]=copyTable(v) end
else
tblRes=datatable
end
return tblRes
end
tab={1,2,3}
print(tab) -->table: 0x1d387e0 tab={1,2,3}
print(nop(tab)) -->table: 0x1d387e0 tab={1,2,3}
print(noop(tab)) -->table: 0x1e76f90 tab={1,2,3}
print(nooop(tab)) -->table: 0x1d387e0 tab={1,2,3,4}
print(tab) -->table: 0x1d387e0 tab={1,2,3,4}
print(copyTable(tab)) -->table: 0x1d388d0
我们可以看到对 table 的引用通过函数(当我只是阅读它或添加东西时)传递不变,除了在 noop() 中,我尝试对现有的进行彻底修改。
我读了Bas Bossink and the answer made by Michael Anderson in this Q/A。将传递或 table 作为参数,他们强调了 "arguments passed by ref" 和 "arguments passed by values and tables are references" 之间的区别,并举例说明了这种差异。
但这到底是什么意思?我们是否有引用的副本,但是这与通过引用有什么区别,因为指向并因此操作的数据仍然是相同的,而不是复制的? noop() 中的机制是否特定于当我们尝试对 table 影响 nil 时,特定于避免删除 table 或在哪些情况下它会触发(我们可以通过 nooop() 看到修改 table 时并不总是这样)?
我的问题:传递 table 的机制究竟如何运作?有没有一种方法可以更有效地复制 table 的数据而没有我的 copyTable 的负担?
参数传入规则Lua和C类似:一切都是传值,但是tables 和用户数据作为指针传递。传递引用的副本在用法上并没有太大区别,但它与通过引用传递完全不同。
比如你专门提出了这部分。
function noop(x)
x={}
return x
end
print(noop(tab)) -->table: 0x1e76f90 tab={1, 2, 3}
您正在将新的 table[1] 的值赋给变量 x
(x
现在拥有一个新的指针值)。你没有改变原来的 table,tab
变量仍然持有指向原来的 table 的指针值。当您从 noop
return 时,您正在传回新的 table 的值,它是空的。 变量保存值,指针是值,不是引用。
编辑:
错过了你的其他问题。不行,如果你想deep-copy一个table,一个类似你写的函数是唯一的办法。当 table 变大时,深拷贝 非常 慢。为避免性能问题,您可以使用像 "rewind tables" 这样的机制来跟踪对它们所做的更改,以便在以后的时间点撤消它们(在回溯上下文的递归中非常有用)。或者,如果您只是需要防止用户搞砸 table 内部结构,请写一个 "freezable" 特征。
[1] 假设 {}
语法是一个构造新 table 和 return 指向新 table.
的函数
如果您确定这 3 个假设 (A) 对于 "tab"(正在复制的 table)有效:
没有table键
t1 = {}
tab = {}
tab[t1] = value
没有重复的table值
t1 = {}
tab = {}
tab.a = t1
tab.b = t1
-- or
-- tab.a.b...x = t1
没有递归 tables:
tab = {}
tab.a = tab
-- or
-- tab.a.b...x = tab
那么您提供的代码是最小的并且几乎是尽可能高效的。
如果 A1 不成立(即您有 table 个键),那么您必须将代码更改为:
function copyTable(datatable)
local tblRes={}
if type(datatable)=="table" then
for k,v in pairs(datatable) do
tblRes[copyTable(k)] = copyTable(v)
end
else
tblRes=datatable
end
return tblRes
end
如果 A2 不成立(即您重复了 table 个值),那么您可以将代码更改为:
function copyTable(datatable, cache)
cache = cache or {}
local tblRes={}
if type(datatable)=="table" then
if cache[datatable] then return cache[datatable]
for k,v in pairs(datatable) do
tblRes[copyTable(k, cache)] = copyTable(v, cache)
end
cache[datatable] = tblRes
else
tblRes=datatable
end
return tblRes
end
不过,如果您有很多重复的大 table,这种方法才会奏效。因此,这是一个评估哪个版本对于您的实际生产场景更快的问题。
如果 A3 不成立(即你有递归 tables),那么你的代码(以及上面的两个调整)将进入无限递归循环并最终抛出堆栈溢出。
最简单的处理方法是保持回溯并在发生 table 递归时抛出错误:
function copyTable(datatable, cache, parents)
cache = cache or {}
parents = parents or {}
local tblRes={}
if type(datatable)=="table" then
if cache[datatable] then return cache[datatable]
assert(not parents[datatable])
parents[datatable] = true
for k,v in pairs(datatable) do
tblRes[copyTable(k, cache, parents)]
= copyTable(v, cache, parents)
end
parents[datatable] = false
cache[datatable] = tblRes
else
tblRes=datatable
end
return tblRes
end
我的处理递归 tables 的深度复制函数的解决方案,保留原始结构可以在这里找到:https://gist.github.com/cpeosphoros/0aa286c6b39c1e452d9aa15d7537ac95
我尝试有效地制作 lua table 的副本。我编写了以下运行良好的函数 copyTable()(见下文)。但是我想我可以使用函数的 "passing by value" 机制来提高效率。我做了一些测试来探索这个机制:
function nop(x)
return x
end
function noop(x)
x={}
return x
end
function nooop(x)
x[#x+1]=4
return x
end
function copyTable(datatable)
local tblRes={}
if type(datatable)=="table" then
for k,v in pairs(datatable) do tblRes[k]=copyTable(v) end
else
tblRes=datatable
end
return tblRes
end
tab={1,2,3}
print(tab) -->table: 0x1d387e0 tab={1,2,3}
print(nop(tab)) -->table: 0x1d387e0 tab={1,2,3}
print(noop(tab)) -->table: 0x1e76f90 tab={1,2,3}
print(nooop(tab)) -->table: 0x1d387e0 tab={1,2,3,4}
print(tab) -->table: 0x1d387e0 tab={1,2,3,4}
print(copyTable(tab)) -->table: 0x1d388d0
我们可以看到对 table 的引用通过函数(当我只是阅读它或添加东西时)传递不变,除了在 noop() 中,我尝试对现有的进行彻底修改。
我读了Bas Bossink and the answer made by Michael Anderson in this Q/A。将传递或 table 作为参数,他们强调了 "arguments passed by ref" 和 "arguments passed by values and tables are references" 之间的区别,并举例说明了这种差异。
但这到底是什么意思?我们是否有引用的副本,但是这与通过引用有什么区别,因为指向并因此操作的数据仍然是相同的,而不是复制的? noop() 中的机制是否特定于当我们尝试对 table 影响 nil 时,特定于避免删除 table 或在哪些情况下它会触发(我们可以通过 nooop() 看到修改 table 时并不总是这样)?
我的问题:传递 table 的机制究竟如何运作?有没有一种方法可以更有效地复制 table 的数据而没有我的 copyTable 的负担?
参数传入规则Lua和C类似:一切都是传值,但是tables 和用户数据作为指针传递。传递引用的副本在用法上并没有太大区别,但它与通过引用传递完全不同。
比如你专门提出了这部分。
function noop(x)
x={}
return x
end
print(noop(tab)) -->table: 0x1e76f90 tab={1, 2, 3}
您正在将新的 table[1] 的值赋给变量 x
(x
现在拥有一个新的指针值)。你没有改变原来的 table,tab
变量仍然持有指向原来的 table 的指针值。当您从 noop
return 时,您正在传回新的 table 的值,它是空的。 变量保存值,指针是值,不是引用。
编辑:
错过了你的其他问题。不行,如果你想deep-copy一个table,一个类似你写的函数是唯一的办法。当 table 变大时,深拷贝 非常 慢。为避免性能问题,您可以使用像 "rewind tables" 这样的机制来跟踪对它们所做的更改,以便在以后的时间点撤消它们(在回溯上下文的递归中非常有用)。或者,如果您只是需要防止用户搞砸 table 内部结构,请写一个 "freezable" 特征。
[1] 假设 {}
语法是一个构造新 table 和 return 指向新 table.
如果您确定这 3 个假设 (A) 对于 "tab"(正在复制的 table)有效:
没有table键
t1 = {} tab = {} tab[t1] = value
没有重复的table值
t1 = {} tab = {} tab.a = t1 tab.b = t1 -- or -- tab.a.b...x = t1
没有递归 tables:
tab = {} tab.a = tab -- or -- tab.a.b...x = tab
那么您提供的代码是最小的并且几乎是尽可能高效的。
如果 A1 不成立(即您有 table 个键),那么您必须将代码更改为:
function copyTable(datatable)
local tblRes={}
if type(datatable)=="table" then
for k,v in pairs(datatable) do
tblRes[copyTable(k)] = copyTable(v)
end
else
tblRes=datatable
end
return tblRes
end
如果 A2 不成立(即您重复了 table 个值),那么您可以将代码更改为:
function copyTable(datatable, cache)
cache = cache or {}
local tblRes={}
if type(datatable)=="table" then
if cache[datatable] then return cache[datatable]
for k,v in pairs(datatable) do
tblRes[copyTable(k, cache)] = copyTable(v, cache)
end
cache[datatable] = tblRes
else
tblRes=datatable
end
return tblRes
end
不过,如果您有很多重复的大 table,这种方法才会奏效。因此,这是一个评估哪个版本对于您的实际生产场景更快的问题。
如果 A3 不成立(即你有递归 tables),那么你的代码(以及上面的两个调整)将进入无限递归循环并最终抛出堆栈溢出。
最简单的处理方法是保持回溯并在发生 table 递归时抛出错误:
function copyTable(datatable, cache, parents)
cache = cache or {}
parents = parents or {}
local tblRes={}
if type(datatable)=="table" then
if cache[datatable] then return cache[datatable]
assert(not parents[datatable])
parents[datatable] = true
for k,v in pairs(datatable) do
tblRes[copyTable(k, cache, parents)]
= copyTable(v, cache, parents)
end
parents[datatable] = false
cache[datatable] = tblRes
else
tblRes=datatable
end
return tblRes
end
我的处理递归 tables 的深度复制函数的解决方案,保留原始结构可以在这里找到:https://gist.github.com/cpeosphoros/0aa286c6b39c1e452d9aa15d7537ac95