пятница, 21 августа 2009 г.

Полное копирование таблиц Lua

Lua - прототипированный язык с динамической типизацией.
Lua не предоставляет программисту средств для манипулирования указателями и ссылками. В Lua следует различать переменные, исходя из того, что в данный момент времени они содержат. Различают скалярные типы(строки, числовые значения) и не скалярные(таблицы, userdata, функции)

  • Примеры работы оператора присваивания со скалярными типами:
local t = 0.5
local p = t
print(t,p)
t = 0.3
print(t,p)
0.5 0.5
0.3 0.5
Аналогично:
local t = "hello"
local p = t
print(t,p)
t = "bye"
print(t,p)
hello hello
bye hello
Таким образом, если источник оператора присваивания есть переменная скалярного типа то приемник будет содержать копию источника. В нашем случае переменная p содержит копию переменной t.
  • Примеры работы оператора присваивания с не скалярными типами:
local t = {"a","b","c"}
local p = t
print(t,p)
for i,val in ipairs(t) do -- покажем все элементы таблицы
print(i,val)
end
t[1] = "W"
print(t,p)
for i,val in ipairs(p) do -- покажем все элементы таблицы
print(i,val)
end
table: 0x8a93028 table: 0x8a93028
1 a
2 b
3 c
table: 0x8a93028 table: 0x8a93028
1 W
2 b
3 c
Таким образом, если источник оператора присваивания есть переменная типа table, userdata или функция(на самом деле переменные содержат лишь адреса в этом случае), то приемник будет содержать адрес того же места памяти, куда ссылался источник. В примере это хорошо видно, содержимое t и p - это адрес 0x8a93028. В силу того, что t и p ссылаются на одно и тоже место в памяти, изменение посредством индексации через t или p не различимо.
На самом деле подобный механизм удобен и экономичен. Но если программист использует объектно-ориентированный подход в Lua, определяя что таблица - это либо прототип либо объект, то он неизбежно сталкивается с проблемами.
Foo =
{
p = "hello world",
s = 2,
}

function Foo:Test1()
print(self.p,self.s)
end

local foo1 = Foo -- считаем, что создаем объект прототипа Foo
foo1.s = 100
foo1:Test1()
local foo2 = Foo -- считаем, что создаем объект прототипа Foo
foo2:Test1() -- ожидаем hello world 2
hello world 100
hello world 100
Таким образом, если прототип не является синглетоном, то поведение является не ожидаемым. Программист считает, что он создает объект или хотя бы копию прототипа Foo, но на самом деле он создает переменную, ссылающуюся на прототип.
deepcopy решает эту проблему:
function deepcopy(object)
local lookup_table = {}
local function _copy(object)
if type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table = {}
lookup_table[object] = new_table
for index, value in pairs(object) do
new_table[_copy(index)] = _copy(value)
end
return setmetatable(new_table, _copy(getmetatable(object)))
end
return _copy(object)
end
И тогда:
Foo =
{
p = "hello world",
s = 2,
}

function Foo:Test1()
print(self.p,self.s)
end

local foo1 = deepcopy(Foo)
foo1.s = 100
foo1:Test1()
local foo2 = deepcopy(Foo)
foo2:Test1()
hello world 100
hello world 2
Будет работать ожидаемо.
Однако в строках 4-5 функции deepcopy:
if type(object) ~= "table" then
return object
мы замечаем, что, если вложенный объект таблицы не является таблицей то копируется его содержимое.
Foo =
{
p = "hello world",
s = 2,
}

function Foo:Test1()
print(self.p,self.s)
end

local foo1 = deepcopy(Foo)
print(foo1.Test1)
local foo2 = deepcopy(Foo)
print(foo1.Test1)
function: 0x8613e98
function: 0x8613e98
Таким образом в объектах foo1 и foo2 функции Test1 суть одно и тоже. В большинстве случаев этот факт не будет мешать программисту и поведение будет ожидаемым. Но если программист использует такие функции, как setfenv или getfenv, или иные средства изменения функций над функциями таблиц, то поведение вновь становится не ожидаемым.
Для того чтобы исправить положение, можно использовать следующий модифицированный вариант deepcopy:
function deepcopymod1(obj)
local lookup_table = {}
local function _copy(object)
if type(object) == "function" then
return loadstring(string.dump(object))
elseif type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table = {}
lookup_table[object] = new_table
for index, value in pairs(object) do
new_table[_copy(index)] = _copy(value)
end
return setmetatable(new_table, _copy(getmetatable(object)))
end
return _copy(obj)
end
Здесь мы учитываем различие функций таблиц.

Замечание
Стоит обратить внимание, что у string.dump(function) есть ограничение. А также после loadstring у копии функции будет окружение отличное от окружения исходной функции, нужное окружение необходимо выставить самостоятельно.

1 комментарий: