If you want to copy inner tables too, it’s a bit more complicated. This is called a deep copy. The basic idea is to call your copier function on any contained tables, so that way you copy the outer table, the inner table, and any tables inside those recursively forever!
I'll take you through writing a deep copier and some common pitfalls.
Here is our first iteration:
local function deepCopy(tbl) local new_tbl = {} for key, value in next, tbl do local new_value if type(value) == 'table' then new_value = deepCopy(value) else new_value = value end new_tbl[key] = new_value end return new_tbl end
In most cases this works! Inner tables are copied, and you have a fancy new table with everything different, it seems.
We have neglected the fact that keys can be anything. Check this out:
local tbl = {} local tbl_key = {} tbl[tbl_key] = "hello!" print(tbl[tbl_key]) -- prints hello!
In this first version, tbl_key won’t be deep copied, it will be shallow copied because we never use deepCopy on keys. Let’s fix that.
local function deepCopy(tbl) local new_tbl = {} for key, value in next, tbl do local new_key, new_value if type(key) == 'table' then new_key = deepCopy(key) else new_key = key end if type(value) == 'table' then new_value = deepCopy(value) else new_value = value end new_tbl[new_key] = new_value end return new_tbl end
Great! Now tables-as-keys works perfectly!
It still won’t work in all cases. Consider this:
local tbl = {} tbl[1] = tbl deepCopy(tbl)
That will result in an infinite loop. deepCopy will call itself on tbl over and over. It sees a table it needs to copy, but doesn’t consider that it’s already been copied!
To solve this, we need a way to “remember” that we already copied a table. Furthermore, we need to “remember” what the new_tbl for the already-copied-table is so that we can use it.
We just discussed the solution a moment ago! We can accomplish this with table keys.
In the following scenario, I’m going to copy tbl1 but not tbl2, then I’ll show that tbl1 was copied and tbl2 was not. Consider:
local already_copied = {} local tbl1 = {"hello"} local tbl2 = {"goodbye"} local new_tbl1 = {unpack(tbl1)} already_copied[tbl1] = new_tbl print(already_copied[tbl1]) -- prints table: ###### which refers to new_tbl1 print(already_copied[tbl2]) -- prints nil
By using tables as keys, and setting the value to the new_tbl, we can check if a table has already been copied and get the new_tbl all in one operation!
To implement this in deepCopy, we’ll have to somehow reference the same already_copied through all of our recursive calls while copying a single table.
This is actually pretty easy. We can just pass the table on as a parameter to each call!
Let’s implement it:
local function deepCopy(tbl, already_copied) if already_copied == nil then -- let it be called with `deepCopy(tbl)` instead of `deepCopy(tbl, {})` already_copied = {} end local new_tbl = {} already_copied[tbl] = new_tbl for key, value in next, tbl do local new_key, new_value if type(key) == 'table' then local existing_copy = already_copied[key] if existing_copy == nil then new_key = deepCopy(key, already_copied) else new_key = existing_copy end else new_key = key end if type(value) == 'table' then local existing_copy = already_copied[value] if existing_copy == nil then new_value = deepCopy(value, already_copied) else new_value = existing_copy end else new_value = value end new_tbl[new_key] = new_value end return new_tbl end
This should work perfectly. Both keys and values are deep copied, and it won’t get stuck in a recursive loop. Awesome! It can be used as easy as local new_table = deepCopy(some_table_here)
This does not copy Instances or any other type that saves state (like Random). This only copies tables.
Now that deep copying is covered, let’s cover metatables.
Metatables are great and neat. They also can’t always be copied, or shouldn’t be copied.
- The
__metatable metamethod can be used to prevent getmetatable from working as you would expect – __metatable can cause getmetatable to return literally any value except nil - The metamethods might be written specifically with reference to the original table and won’t work with others. e.g.
local function new_object(a) local tbl = {[1] = a} local metatable = { __index = { print = function() print(tbl[1]) end } } setmetatable(tbl, metatable) return tbl end local obj1 = new_object(5) local obj2 = {unpack(obj1)} setmetatable(obj2, getmetatable(obj1)) obj1:print() -- prints 5 obj1[1] = 7 obj1:print() -- prints 7 obj2[1] = 29 obj2:print() -- prints 7, because it's still referring to obj1 in its metatable.
If an object has a metatable, then you probably should not be copying it this way.
Let’s implement that in our deep copier!
local function deepCopy(tbl, already_copied) if already_copied == nil then -- let it be called with `deepCopy(tbl)` instead of `deepCopy(tbl, {})` already_copied = {} end local meta = getmetatable(tbl) if meta ~= nil then -- uh oh, it has a metatable! -- let's just keep a reference to it instead :shrug: already_copied[tbl] = tbl return tbl end local new_tbl = {} already_copied[tbl] = new_tbl for key, value in next, tbl do local new_key, new_value if type(key) == 'table' then local existing_copy = already_copied[key] if existing_copy == nil then new_key = deepCopy(key, already_copied) else new_key = existing_copy end else new_key = key end if type(value) == 'table' then local existing_copy = already_copied[value] if existing_copy == nil then new_value = deepCopy(value, already_copied) else new_value = existing_copy end else new_value = value end new_tbl[new_key] = new_value end return new_tbl end
You could, alternatively, allow deep copying by implementing a :clone method. Keep in mind though that because it has a metatable, indexing __index might error! We’ll have to wrap it in a pcall…
local function deepCopy(tbl, already_copied) if already_copied == nil then -- let it be called with `deepCopy(tbl)` instead of `deepCopy(tbl, {})` already_copied = {} end local meta = getmetatable(tbl) if meta ~= nil then local copy pcall(function() if tbl.clone then copy = tbl:clone() end end) if copy ~= nil then already_copied[tbl] = copy return copy else already_copied[tbl] = tbl return tbl end end local new_tbl = {} already_copied[tbl] = new_tbl for key, value in next, tbl do local new_key, new_value if type(key) == 'table' then local existing_copy = already_copied[key] if existing_copy == nil then new_key = deepCopy(key, already_copied) else new_key = existing_copy end else new_key = key end if type(value) == 'table' then local existing_copy = already_copied[value] if existing_copy == nil then new_value = deepCopy(value, already_copied) else new_value = existing_copy end else new_value = value end new_tbl[new_key] = new_value end return new_tbl end
Right now, :clone can’t return nil. Let’s fix that. We’ll need some value to stand in for nil…
local Nil = {} local function deepCopy(tbl, already_copied) if already_copied == nil then -- let it be called with `deepCopy(tbl)` instead of `deepCopy(tbl, {})` already_copied = {} end local meta = getmetatable(tbl) if meta ~= nil then local copy pcall(function() if tbl.clone then copy = tbl:clone() if copy == nil then copy = Nil end end end) if copy ~= nil then already_copied[tbl] = copy return copy else already_copied[tbl] = tbl return tbl end end local new_tbl = {} already_copied[tbl] = new_tbl for key, value in next, tbl do local new_key, new_value if type(key) == 'table' then local existing_copy = already_copied[key] if existing_copy == nil then new_key = deepCopy(key, already_copied) else if existing_copy == Nil then new_key = nil else new_key = existing_copy end end else new_key = key end if type(value) == 'table' then local existing_copy = already_copied[value] if existing_copy == nil then new_value = deepCopy(value, already_copied) else if existing_copy == Nil then new_value = existing_copy else new_value = existing_copy end end else new_value = value end if new_key ~= nil then new_tbl[new_key] = new_value end end return new_tbl end
That got really long really fast! That should pretty much covers everything.
Ideally you won’t be deep-copying things often. If you ever are deep-copying things, then you want to understand your data structure well enough to know what you need to include and ideally use something simpler than this. Once we consider metatables, we realize that there’s no catch-all way to copy a table in Lua.