Благодаря комментариям даурниматора, я думаю, что нашел способ заставить метаметоды «следовать» __index
так, как я хочу. Это сжато в этой функции:
local metamethods = {
'__add', '__sub', '__mul', '__div', '__mod', '__pow', '__unm', '__concat',
'__len', '__eq', '__lt', '__le', '__call', '__gc', '__tostring', '__newindex'
}
function setindirectmetatable(t, mt)
for _,m in ipairs(metamethods) do
rawset(mt, m, rawget(mt,m) or function(...)
local supermt = getmetatable(mt) or {}
local index = supermt.__index
if(type(index)=='function') then return index(t,m)(...) end
if(type(index)=='table') then return index[m](...) end
return nil
end)
end
return setmetatable(t, mt)
end
Надеюсь, это достаточно просто. Когда устанавливается новый метатабль, он инициализирует его всеми метаметодами (без замены существующих). Эти метаметоды подготовлены для «передачи» запросов «родительским метатаблицам».
Это самое простое решение, которое я мог найти. Ну, на самом деле я нашел решение, которое использовало меньше символов и было немного быстрее, но оно включало черную магию (оно включало метатабельные функции, разыменовывающие себя внутри своих собственных тел), и оно было гораздо менее читабельным, чем это.
Если кто-нибудь найдет более короткую и простую функцию, которая делает то же самое, я с удовольствием предоставлю ему ответ.
Использование простое: замените setmetatable
на setindirectmetatable
, когда хотите, чтобы оно "пошло вверх":
mt1 = {__tostring=function(x) return x.name or "no name" end }
mt2 = {}
setmetatable(mt2, {__index=mt1})
x = {name='x'}
y = {name='y'}
setmetatable(x, mt1)
setindirectmetatable(y, mt2) -- only change in code
print(x) -- prints "x"
print(mt2.__tostring(y)) -- prints "y"
print(y) -- prints "y"
Небольшое предупреждение: setindirectmetatable
создает метаметоды на mt2. Изменение этого поведения, чтобы сделать копию, и mt2 остается неизменным, должно быть тривиальным. Но для их целей лучше всего настроить их по умолчанию.