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