views:

74

answers:

2

I'm writing a lua script, and one of the things it does is copy a table into a table of tables, and apply a couple transformations to it. What's odd though is when i go to use one of those tables later (and modify some of it's properties), changes will also seem to show up in other tables! Code:

-- thanks to http://stackoverflow.com/questions/1283388/lua-merge-tables/1283608#1283608
-- tableMerge:
-- merges two tables, with the data in table 2 overwriting the data in table 1
function tableMerge(t1, t2)
    for k,v in pairs(t2) do
     if type(v) == "table" then
      if type(t1[k] or false) == "table" then
       tableMerge(t1[k] or {}, t2[k] or {})
      else
       t1[k] = v
      end
     else
      t1[k] = v
     end
    end
    return t1
end

--tableCopy:
--takes a table and returns a complete copy including subtables.
function tableCopy(t)
    return tableMerge({}, t)
end

local t1 = { a = 1, b = true, c = "d", e = { f = 2 } }
local t2 = tableCopy(t1)
t2.b = false
t2.e.f = 1
print(t1.b) -- prints true as it should
print(t1.e.f) -- prints 1!

[removed other code for reasons of the information it contains, and this a good reproduction of the bug]

so is it a bug in my code or something? i can't figure it out....

+4  A: 

This is how Lua tables work - they don't get copied around, only references to the tables are passed to functions or stored in tables instead. If you are familiar with .NET terminology, you could say that tables are "reference types". Observe:

function modtable(t)
    t.hello = "world"
end

local t = { hello = "no!"; }
modtable(t)
print(t.hello)

This prints "world" because the modtable function gets a reference to the table and not a copy. The same thing happens when you try to store table in another table

local t = { hello = "no!"; }
local bigT = { innerTable = t; }
bigT.innerTable.hello = "world"
print(t.hello)

t.hello = "double world";
print(bigT.innerTable.hello);

This will print

world 
double world

because t and bigT.innerTable are essentially the same table.

If you want copies of the tables that you can modify independently from one-another you can write a small function to duplicate a table

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

local t = { hello = "no!"; }
local bigT = { innerTable = deep_copy_table(t); }
bigT.innerTable.hello = "world"
print(t.hello)

t.hello = "double world";
print(bigT.innerTable.hello);

This will print "no!" and "world" - t and bigT.innerTable are different tables now.

sbk
+1 for clear exposition and examples.
RBerteig
But what's odd is that just before the problem area i explicitly make a copy of the table i'm modifying. Maybe that's broken? BTW, that's the strangest formatting convention i've seen, with the "then" on the next line...
RCIX
A: 

OK, sorry for the double answer - the previous answer is still good, although it's an answer to a different question.

I spotted the problem in your code, it's the tableMerge function as expected:

    if type(v) == "table" then
            if type(t1[k] or false) == "table" then
                    tableMerge(t1[k] or {}, t2[k] or {})
            else
                    t1[k] = v -- this is the problematic line
            end
    else

So, if t1[k] is not a table, you just assign v to it and you end up with a reference to v instead of a copy. This effectively makes your tableCopy function shallow copy instead of deep copy. Since you are overwriting t1[k] anyway, this seems like a good implementation of the tableMerge function:

function tableMerge(t1, t2)
    for k,v in pairs(t2) do
        if type(v) == "table" then
            if type(t1[k]) ~= "table" then -- if t1[k] is not a table, make it one
                t1[k] = {}
            end
            tableMerge(t1[k], t2[k])
        else
            t1[k] = v
        end
    end
    return t1
end

This should hopefully fix the problem at hand.

Some other random thoughts on your code, if you don't mind:

  • in the original implementation of mergeTable: type(t1[k] or false). The t1[k] or false trickery is pointless here, type() can handle nil values just fine (it will return "nil" for nil)

  • t2[k] can never be nil. Tables in Lua cannot contain nil as a value, pairs() will never return a pair with nil as key or value. The t2[k] or false is again kind of pointless.

  • I spotted this line near the end of the first script: table.remove(v.Weapons). I'm guessing you need second parameter for the call to table.remove

Oh, and one final piece of advice: don't just drop 200 lines of code and expect people to debug it for you - no one will bother. Try to narrow down the problematic code. You already suspected the tableMerge function, if you had taken it out by itself and ran a simple test on it you would have spotted the problem.

Happy coding!

sbk
Someone suggested that there were bugs in the old code not checking for types often enough, i guess i was too safe. I originally made a test and it didn't show the bug (i didn't suspect that only subtables weren't getting copied correctly), but i realized what was wrong just now and i swapped out the code for the (now fixed to show the bug) test case. If you pass table.remove only one argument it removes the item at the last index in the table. Thank you
RCIX