3

我尝试有效地制作 lua 表的副本。我编写了以下运行良好的函数 copyTable()(见下文)。但我想我可以使用函数的“按值传递”机制获得更高效的东西。我做了一些测试来探索这种机制:

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

我们可以看到,对表的引用通过函数(当我只是阅读它或添加东西时)没有改变,除了在 noop() 中我尝试对现有的进行彻底修改。

我在这个 Q/A中阅读了Bas Bossink和Michael Anderson的回答。关于传递或表作为参数,他们强调了“通过 ref 传递的参数”和“通过值和表传递的参数是引用”之间的区别,并举例说明了这种区别。

但这究竟意味着什么?我们是否有引用的副本,但是由于指向并因此被操作的数据仍然是相同的,而不是复制的,这与传递 ref 有什么区别?当我们尝试将 nil 影响到 table 时,noop() 中的机制是否特定于避免删除 table 或在哪些情况下触发(我们可以通过 nooop() 看到,当表已修改)?

我的问题:传递表格的机制如何真正起作用?有没有一种方法可以更有效地复制表的数据而无需我的 copyTable 负担?

4

2 回答 2

1

Lua 中的参数传递规则与 C 类似:一切都按值传递,但表和用户数据作为指针传递。传递引用的副本在用法上看起来并没有太大的不同,但它与通过引用传递完全不同。

例如,您专门提出了这一部分。

function noop(x)
  x={}
  return x
end
print(noop(tab))      -->table: 0x1e76f90 tab={1, 2, 3}

您正在将新表 [1] 的值分配给变量xx现在保存一个新的指针值)。你没有改变原始表,tab变量仍然保存指向原始表的指针值。当您返回时,noop您将传回新表的值,该表为空。变量保存值,指针是值,而不是引用。

编辑:

错过了你的另一个问题。不,如果你想深拷贝一个表,一个类似于你写的函数是唯一的方法。当表变大时,深拷贝非常慢。为了避免性能问题,您可以使用类似“rewind tables”的机制,它会跟踪对它们所做的更改,以便可以在以后的时间点撤消它们(在递归回溯上下文中非常有用)。或者,如果您只需要防止用户搞砸表格内部结构,请编写一个“可冻结”特征。

[1] 想象一下{}语法是一个构造新表并返回指向新表的指针的函数。

于 2017-02-11T17:36:15.243 回答
0

如果您确定这 3 个假设 (A) 对“tab”(正在复制的表)有效:

  1. 没有表键

    t1 = {}
    tab = {}
    tab[t1] = value
    
  2. 没有重复的表值

    t1 = {}
    tab = {}
    tab.a = t1
    tab.b = t1
    -- or
    -- tab.a.b...x = t1
    
  3. 没有递归表:

    tab = {}
    tab.a = tab
    -- or
    -- tab.a.b...x = tab
    

那么您提供的代码是最小的并且几乎是尽可能高效的。

如果 A1 不成立(即您有表键),那么您必须将代码更改为:

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 不成立(即您有重复的表值),那么您可以将代码更改为:

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

但是,这种方法只有在您有很多重复的大表时才会奏效。因此,对于您的实际生产场景,评估哪个版本更快是一个问题。

如果 A3 不成立(即您有递归表),那么您的代码(以及上面的两个调整)将进入无限递归循环并最终引发堆栈溢出。

最简单的处理方法是保持回溯并在表递归发生时抛出错误:

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

我的处理递归表的 deepcopy 函数的解决方案,保留原始结构可以在这里找到:https ://gist.github.com/cpeosphoros/0aa286c6b39c1e452d9aa15d7537ac95

于 2017-08-24T16:46:06.577 回答