如何通过值复制Lua表?

70

最近我写了一些类似以下的 Lua 代码:

local a = {}
for i = 1, n do
   local copy = a
   -- alter the values in the copy
end
显然,这不是我想要的,因为在Lua中变量保存的是匿名表的引用而不是表本身的值。这在《Lua编程》明确说明了,但我忘记了这一点。所以问题是,我应该写什么代码来获取a表中的值的副本,而不是像copy = a那样获取表的引用?
16个回答

50

表格复制有许多潜在的定义。这取决于您想要简单还是深度复制,是否想要复制、共享或忽略元表等。没有一种实现可以满足所有人。

一种方法是简单地创建一个新表格并复制所有键/值对:

function table.shallow_copy(t)
  local t2 = {}
  for k,v in pairs(t) do
    t2[k] = v
  end
  return t2
end

copy = table.shallow_copy(a)

请注意,应使用 pairs 而不是 ipairs。因为 ipairs 只会迭代表中的一部分键(也就是以1开始的连续正整数键按升序排序)。


2
这段代码是不正确的。我使用它时遇到了一个特定的问题:如果你在表格中嵌套了表格,你需要检查v的类型,并且递归地使用table.copy。我将会在下面单独发布代码。 - scippie
21
这段代码并没有错误,如果你不需要进行递归复制,这是一种完全有效的复制表格的方法。如果你需要递归复制,那么可以将函数改为递归实现。之所以它没有被包含在标准库中,是因为"表格复制"没有一个统一的定义。 - Doub
3
我能理解@scippie的观点。也许需要一个更好地表示正在进行的复制类型的名称?我将更新名称为“shallow_copy”;如果有任何异议,请告诉我。 - greatwolf
1
不,那正是我的观点。@greatwolf 做了一个好选择。我取消了踩的评价。 - scippie
关键值必须是数字吗? - Jax
不,pairs函数将迭代所有具有非nil值的键。 - Doub

32

只是为了阐明这一点,我的个人 table.copy 也关注元表:

function table.copy(t)
  local u = { }
  for k, v in pairs(t) do u[k] = v end
  return setmetatable(u, getmetatable(t))
end

没有被广泛认可为“标准”的复制功能。

5
注意,将标准命名空间如table进行更改或扩展被认为是一种不良实践。 - Alexander Gladysh
2
这个代码可以正常工作,直到有人设置了元表的 __metatable 属性。 - Eric
еҸҰеӨ–пјҢд»ҺLua 5.2ејҖе§ӢпјҢдҪ еә”иҜҘзӣҙжҺҘдҪҝз”ЁnextеҮҪж•°пјҢеӣ дёәpairsзҡ„иЎҢдёәеҸҜиғҪдјҡеҸ—еҲ°е…ғж–№жі•__pairsзҡ„еҪұе“ҚгҖӮжӯӨеӨ–пјҢдҪ еҸҜд»ҘйҖҡиҝҮдҪҝз”Ёdebug.getmetatableжқҘи§ЈеҶі__metatableй—®йўҳ...дҪҶжҲ‘дёҚзЎ®е®ҡиҝҷж ·еҒҡжҳҜеҗҰеҗҲйҖӮгҖӮ - Deco

29

为了玩一个可读性代码压缩的小游戏,这里提供了一份处理标准棘手情况的简短代码:

  • 以表格作为键,
  • 保留元表,和
  • 递归表。

我们可以用7行代码来实现:

function copy(obj, seen)
  if type(obj) ~= 'table' then return obj end
  if seen and seen[obj] then return seen[obj] end
  local s = seen or {}
  local res = setmetatable({}, getmetatable(obj))
  s[obj] = res
  for k, v in pairs(obj) do res[copy(k, s)] = copy(v, s) end
  return res
end

这个代码片段简短地介绍了Lua深度复制操作。

另一个有用的参考是这个Lua用户维基页面,其中包括一个示例,介绍如何避免__pairs元方法。


如果他只是复制一个对象,为什么会有两个参数? - claudekennilol
@claudekennilol 第二个参数是为了在递归调用时被“外部”调用者忽略,仅用于避免在单个表中多次发生重复深度复制的表。例如:a = {<任何值>} b = {k1=a, k2=a}。如果b2是b的副本,则保持b2.k1 == b2.k2很好。额外的“seen”参数有助于实现这一点。 - Tyler
1
这在元表中可能会出现严重问题,特别是因为您首先设置元表,然后再分配字段...(__newindex可能会做各种有趣的事情)。此外,__pairs__index可能会干扰迭代,因此最好使用for k,v in next,obj,nil do来复制实际内容(这相当于rawget用于pairs),然后在最后设置元表(这将使事情看起来相同)。另外:为什么不将seen = seen or {}作为第一行(这更可读),然后if seen and seen[x]--> if seen[x] - nobody
1
@nobody:简短回答:这是一段简短而合理的代码,适用于许多在SO页面上找到此问题的人。如果他们想要深度复制一个带有令人惊讶的赋值/迭代元方法的表格,我会说他们自愿进入了应该知道自己在做什么的领域。你关于将seen行放在第一位的建议很好。我认为我写成现在这样是因为我想保持行数短(因此使用s作为变量名),但也清楚地说明了该变量的含义;给它两个名称是我的解决方案(这样行数就相同了)。 - Tyler
@Tyler 哈哈,作为一个自愿进入那个领域的人,我仍在谷歌上搜索“如何复制Lua表格”。但更严肃的是,这种情况可能会影响使用可能使用__newindex和__index进行访问器样式语法的库的人。 - Orlando

23

完整的深拷贝版本,处理所有三种情况:

  1. 表格循环引用
  2. 键也是表格
  3. 元表

一般版本:

local function deepcopy(o, seen)
  seen = seen or {}
  if o == nil then return nil end
  if seen[o] then return seen[o] end

  local no
  if type(o) == 'table' then
    no = {}
    seen[o] = no

    for k, v in next, o, nil do
      no[deepcopy(k, seen)] = deepcopy(v, seen)
    end
    setmetatable(no, deepcopy(getmetatable(o), seen))
  else -- number, string, boolean, etc
    no = o
  end
  return no
end

或者表格版本:

function table.deepcopy(o, seen)
  seen = seen or {}
  if o == nil then return nil end
  if seen[o] then return seen[o] end


  local no = {}
  seen[o] = no
  setmetatable(no, deepcopy(getmetatable(o), seen))

  for k, v in next, o, nil do
    k = (type(k) == 'table') and k:deepcopy(seen) or k
    v = (type(v) == 'table') and v:deepcopy(seen) or v
    no[k] = v
  end
  return no
end

基于 lua-users.org/wiki/CopyTableAlan Yates 的函数。


欢迎来到[SO],感谢您的建议!看起来seen变量是一种缓存系统。我得试试。 - Jon Ericson
这在我的项目中运行良好,希望您能提供反馈,如果有任何问题。 - islet8
3
希望我之前能往下滚一点再写这篇回答!(我认为这应该成为被采纳的答案。) - yoyo

10

一个可选的深度、图形通用、递归版本:

function table.copy(t, deep, seen)
    seen = seen or {}
    if t == nil then return nil end
    if seen[t] then return seen[t] end

    local nt = {}
    for k, v in pairs(t) do
        if deep and type(v) == 'table' then
            nt[k] = table.copy(v, deep, seen)
        else
            nt[k] = v
        end
    end
    setmetatable(nt, table.copy(getmetatable(t), deep, seen))
    seen[t] = nt
    return nt
end

也许元表拷贝也应该是可选的吗?

2
在迭代之前,您需要将 nt 添加到 seen 中,否则会出现堆栈溢出的情况。基本上,删除第15行,并在第6行后插入它。 - Mud
我只是在考虑“但如果t是它自己的元表”的情况,但是如果它只是local t = {}; t [1] = t或其他类似情况,这也是个问题。此外,在查看http://lua-users.org/wiki/CopyTable之后,我发现您忘记复制键,这些键可能是表本身。 - Jasmijn

7

这是我实际所做的:

for j,x in ipairs(a) do copy[j] = x end

Doub提到,如果您的表键不是严格单调递增的,则应该使用pairs而不是ipairs
我还发现一个更加健壮的deepcopy函数:
function deepcopy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, deepcopy(getmetatable(orig)))
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end

它通过递归调用自身来处理表和元表(这是它自己的奖励)。其中一个聪明的地方是,你可以传递任何值给它(无论是表还是其他),它都能正确地复制。然而,代价是它可能会溢出堆栈。因此,可能需要一个更加健壮的(非递归)函数。但对于想要将数组复制到另一个变量的简单情况来说,这已经是过度设计了。

请注意,此版本的深度复制函数不适用于自引用(递归)表格。(如果遵循面向对象的Lua的通用实践,这就是您得到的。) - yoyo
对于堆栈溢出问题,将函数转换为使用尾调用可以解决此问题。然而,这可能不是可能的。 - Oliver

4

不要忘记函数也是引用,所以如果你想完全“复制”所有的值,你需要获取单独的函数;然而,我所知道的唯一复制函数的方法是使用loadstring(string.dump(func)),但根据Lua参考手册,这种方法不适用于带有upvalues的函数。

do
    local function table_copy (tbl)
        local new_tbl = {}
        for key,value in pairs(tbl) do
            local value_type = type(value)
            local new_value
            if value_type == "function" then
                new_value = loadstring(string.dump(value))
                -- Problems may occur if the function has upvalues.
            elseif value_type == "table" then
                new_value = table_copy(value)
            else
                new_value = value
            end
            new_tbl[key] = new_value
        end
        return new_tbl
    end
    table.copy = table_copy
end

4

很遗憾,stdlib 项目的文档相对较少,但它为标准 Lua 发行版附带的多个库提供了许多有价值的扩展。其中包括几个不同主题的表格复制和合并变体。

该库也包含在 Lua for Windows 发行版中,并且可能是任何严肃的 Lua 用户工具箱的一部分。

手动实现此类内容时要确保正确处理元表。对于简单的表格作为结构的应用程序,您可能没有任何元表,使用 pairs() 的简单循环就是可接受的答案。但是,如果表格用作树,或包含循环引用或元表,则情况会变得更加复杂。


2

警告:标记的解决方案是不正确的

当表格包含表格时,仍将使用对这些表格的引用。我花了两个小时搜索我所犯的错误,而实际上是因为使用了以上代码。

因此,您需要检查该值是否为表格。如果是,则应递归调用table.copy!

这是正确的table.copy函数:

function table.copy(t)
  local t2 = {};
  for k,v in pairs(t) do
    if type(v) == "table" then
        t2[k] = table.copy(v);
    else
        t2[k] = v;
    end
  end
  return t2;
end

注意:当表格包含函数或其他特殊类型时,此方法可能不完整,但这对大多数人来说并不是必要的。上述代码易于适应需要的人。

4
我认为说“不正确”有点过头了。这取决于表格内部的内容以及您想要复制什么。您会注意到,其他几个答案也考虑了表格的递归复制问题。还有一些考虑到了元表和函数。 - Jon Ericson
2
在我看来,原帖并没有提到表格的内容。因此,回答应该尽可能完整,而选定的答案并不完整。像我这样的其他人会搜索这个问题,并采用选定的答案,在自己的代码中寻找错误,而实际上错误并不在他们的代码中。 - scippie
说实话,我是这个帖子的发起人,我没有考虑过递归复制包含表格的表格的情况。其他几个答案已经很好地解决了这个问题。 - Jon Ericson

2
我认为Lua标准库中没有'table.copy()'的原因是该任务不够明确。正如此处所示,可以进行“一级深度”的复制(您已经完成了这个操作),深层复制并且不关心可能存在的重复引用,还有元表。
就我个人而言,我仍然希望它们提供一个内置函数。只有当人们对其语义不满意时,他们才需要自己去做。虽然很少,但人们确实需要按值复制的需求。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接