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 у копии функции будет окружение отличное от окружения исходной функции, нужное окружение необходимо выставить самостоятельно.
Спасибо большое
ОтветитьУдалить