В Lua ООП нет. И оно, в общем-то и не нужно: удобной модульности и функций первого класса достаточно для реализации многих вещей. На этом можно было бы и закончить, но пост не про это. В данном случае я распишу работу с метатаблицами, где в качестве примера шаг за шагом будет реализовываться системка по работе с классами в несколько таком python-стиле. Для понимания нужен хотя бы основной базис языка: таблицы, upvalues.
Вариант влоб
Начать можно с самого простого примера:
local Obj = {}
function Obj.spam()
print 'Hello world'
end
--[[ Аналогично можно написать и так:
local Obj = {
spam = function()
print 'Hello world'
end,
}
]]
Obj.spam()
-- Hello world
Мы получили таблицу с одним ключом, значением которого является функция. Однако внутри самой Obj.spam нельзя получить ссылку на сам Obj (кроме как по имени за счет upvalue), потому что пока нет никаких this/self и т.п. внутри функции.
Мы можем «реализовать» это сами:
local Obj = {}
function Obj.spam(self)
print(self)
end
Obj.spam(Obj)
или предоставить это lua:
local Obj = {}
function Obj.spam(self)
print(self)
end
function Obj:spam2()
print(self)
end
Obj:spam() -- эквивалентно Obj['spam'](Obj), т.е. не просто вызов метода, а сначала получение поля по имени, а затем его вызов, как функции.
Obj:spam2()
Obj.spam(Obj)
-- table: 0x417c7d58
-- table: 0x417c7d58
-- table: 0x417c7d58
Результатом работы будет одна и та же ссылка, т.к. все три self одинаковы.
Явное использование a:b вместо a.b(a) можно использовать, при желании, для визуального разграничения методов класса Obj.foo(cls) и методов инстанции a.foo(self).
Наивный вариант конструктора мог бы выглядеть так:
local Obj = {
var = 0,
}
function Obj:new(val)
self:set(val or 0)
return self
end
function Obj:set(val)
self.var = val
end
function Obj:print()
print(self.var)
end
local a = Obj:new(42)
a:print()
local b = Obj:new(100500)
b:print()
a:print()
-- 42
-- 100500
-- 100500
Происходит переиспользование одной и той таблицы, что приводит к замене a.var внутри b.set. Для разделения нужно выдавать в new новую таблицу:
local Obj = {
var = 0,
}
function Obj:set(val)
self.var = val
end
function Obj:print()
print(self.var)
end
function Obj:new(val)
-- каждый раз создаем новую таблицу
local inst = {}
-- добавляем в эту таблицу все, что есть в Obj
for k, v in pairs(self) do
inst[k] = v
end
inst.new = nil -- для запрета создания инстанций из инстанций. можно и оставить :)
inst:set(val or 0)
return inst
end
local a = Obj:new(42)
a:print()
local b = Obj:new(100500)
b:print()
a:print()
-- 42
-- 100500
-- 42
Это работает, но уж больно криво, да и нужно повторять каждый раз.
Метатаблицы
В Lua для каждой таблицы (и userdata, но сейчас не про них речь) можно задавать метатаблицу, описывающую поведение данной таблицы в особенных случаях. Такими случаями могут быть использование в арифметике (перегрузка операторов), конкатенация как строк и т.д. В качестве небольшого примера перегрузки операторов и приведения к строке:
local mt = {
__add = function(op1, op2)
local op1 = type(op1) == 'table' and op1.val or op1
local op2 = type(op2) == 'table' and op2.val or op2
return op1 + op2
end,
__tostring = function(self)
return tostring(self.val)
end,
}
local T = {
val = 0,
new = function(self)
local inst = {}
for k, v in pairs(self) do
inst[k] = v
end
-- метатаблица не является явным полем таблицы, ее нужно назначать явно
setmetatable(inst, getmetatable(self))
return inst
end,
}
setmetatable(T, mt)
local a = T:new()
a.val = 2
local b = T:new()
b.val = 3
print(a)
print(b)
print(a + b)
print(a + 10)
print(100 + b)
-- 2
-- 3
-- 5
-- 12
-- 103
В данном случае нас интересует ключ __index, используемый при обращении к не существующему ключу таблицы, который внутри lua используется следующим образом:
- Если значением ключа является функция, то та вызывается с передачей ей таблицы и искомого ключа. Результат работы функции используется как значение ключа. Повторное обращение по этому же ключу вновь вызывает функцию (никакого «кеширования»);
- Если значением ключа является другая таблица, то ключ ищется в ней. Если ключ не находится, то у данной таблицы рекурсивно проверяется ее метатаблица, и так далее.
Самый настоящий We need to go deeper.
Такой подход позволяет отделить описание класса от создания его экземпляра:
local T = {}
local T_mt = {
__index = T, -- если у таблицы нет ключа, то следует посмотреть в другой таблице
}
function T.create()
-- setmetatable возвращает свой первый параметр в качестве результата
return setmetatable({}, T_mt)
end
function T:set(val)
self.val = val or 0
end
function T:print()
print(self.val)
end
local a = T.create()
a:set(42)
local b = T.create()
b:set(100500)
a:print()
b:print()
a:print()
-- поле инстанции
a.foo = 7
print(a.foo)
print(b.foo)
-- поле класса
T.bar = 7
print(a.bar)
print(b.bar)
Получаемые a и b являются пустыми таблицами, не имеющими ключей new, set и print. Данные методы хранятся в общей таблице T. При таком подходе вызов a:print() на самом деле разворачивается в (только итоговая ветвь исполения):
getmetatable(a).__index.print(a)
Внутри lua это выполняется очень быстро.
При необходимости получить значение только из таблицы, не задействуя магию метатаблиц, можно заменить a.bar на rawget(a, 'bar') / rawset(a, 'bar', value).
В качестве дополнительной приятной мелочи можно реализовать более привычный синтаксис конструкторов:
local T = {}
setmetatable(T, {
__call = function(cls)
return cls.create()
end,
})
-- Все! Теперь вместо T.create() можно писать просто T():
local a = T()
local b = T()
Развитие идеи
Теперь можно попробовать собрать все это воедино в общий генератор классов, который будет выглядеть так:
local OOP = {}
function OOP.class(struct)
-- магия
return cls -- возвращаем класс, не инстанцию
end
-- создаем класс из описания публичных полей и методов инстанции
local A = OOP.class {
val = 0,
set = function(self, val)
self.val = val or 0
end,
print = function(self)
print(self.val)
end,
}
-- создаем и используем
local a = A:create()
a:print()
a:set(42)
a:print()
Реализация в данном объеме весьма простая:
function OOP.class(struct)
local struct = struct or {}
local cls = {}
local function _create_instance()
local inst = {}
for k, v in pairs(struct) do
inst[k] = v
end
inst.__class = cls
return inst
end
setmetatable(cls, {
__index = {
create = _create_instance, -- метод класса, не инстанции
},
__call = function(cls)
return cls:create() -- сахар синтаксиса конструктора
end,
})
return cls
end
Всего и делов-то.
Для методов класса можно сохранить ссылку на класс внутри таблицы инстанции и вопользоватся ей в последствии:
-- ...
local function _create_instance()
local inst = {}
-- ...
inst.__class = cls
-- ...
end
-- ...
A.clsMeth = function(cls)
print('Hello')
end
-- ...
a.__class:clsMeth()
-- a.clsMeth() не доступно
Гораздо интереснее ситуация с наследованием. Пока разберем единичное:
-- метод исключительно ради красивого синтаксиса. необходимости в нем нет
function OOP.subclass(parent)
return function(struct)
return OOP.class(struct, parent)
end
end
local A = OOP.class {
-- ...
}
local B = OOP.subclass(A) { -- B является потомком A
welcome = function(self)
print('Welcome!')
self:print() -- вызов метода потомка как своего
end,
}
local b = B()
b:print()
b:set(100500)
b:welcome()
Для реализации нужно внести не так уж и много правок:
function OOP.class(struct, parent) -- 1. передаем данные по родителю
local struct = struct or {}
local cls = {}
local function _create_instance()
local base = parent and parent:create() or nil -- 2. при создании инстанции создаем ее предка
local inst = {}
-- 3. берем из родителя все его публичные поля
if base then
for k, v in pairs(base) do
inst[k] = v
end
end
for k, v in pairs(struct) do
inst[k] = v
end
inst.__class = cls
return inst
end
setmetatable(cls, {
__index = setmetatable( -- 4. метатаблица получает собственную метатабалицу
{
create = _create_instance,
}, {
-- если чего нет у текущего класса, то ищем у предка
__index = function(_, key)
if parent then
return parent[key]
end
end,
}
),
__call = function(cls)
return cls:create()
end,
})
return cls
end
Для создания собственных явных конструкторов опишем метод new и будем его вызывать при создании инстанции:
-- ...
setmetatable(cls, {
-- ...
__call = function(cls, ...)
local inst = cls:create()
-- если есть конструктор - вызываем его
local new = inst.new
if new then
new(inst, ...)
end
return inst
end,
})
-- ...
local A = OOP.class {
new = function(self, text)
text = text or ''
print('Hello ' .. text)
end,
}
local B = OOP.subclass(A) {
}
A('world')
B('traveler')
-- Hello world
-- Hello traveler
Автоматического вызова конструктора (да и вообще любого другого метода) предка мы не реализовывали, соотвественно
local B = OOP.subclass(A) {
new = function(self, text)
print('B says ' .. tostring(text))
end,
}
B('spam')
не приведет к вызову A.new. Для этого опять нужно лишь внести небольшое дополнение в логику работы, реализовыв метод инстанции super :)
local B = OOP.subclass(A) {
new = function(self, text)
print('B says ' .. tostring(text))
self:super('from B')
end,
}
-- ...
local function super_func(self, ...)
local frame = debug.getinfo(2)
local mt = getmetatable(self)
assert(mt and mt.__base, 'There are no super method')
local func = mt.__base[frame.name]
return func and func(self, ...) or nil
end
-- ...
local function _create_instance()
-- ...
-- вместо явного объявления inst.super выносим метод в метатаблицу, чтобы он не выглядел как частью структуры.
-- но это позволяет объявить одноименный метод/поле.
-- можно добавить проверку имени при обходе pairs(struct), если необходимо. но от a.super = x это не спасет.
local inst = setmetatable({}, {
__base = base,
__index = {
super = super_func,
},
})
-- ...
super вызывается без указания имени вызываемого метода. Для его получения используется модуль debug.
Если не хочется его использовать (или lua запущена без него), то можно явно передавать имя метода.
debug.getinfo() используется для получения краткой информации о запрошенном уровне стека: 0 — текущий (super_func), 1 — уровень, где вызвали super_func,… Нам нужно имя функции, из которой была вызвана super, т.е. поле name второго уровня стека.
Теперь можно вызывать любые родительские методы, не только конструктор :)
Для реализации private полей и методов можно использовать подход на основе соглашения об именовании как в python, или воспользоваться истинным сокрытием через область видимости модуля, или вообще через upvalues:
local A = OOP.class((function()
-- нет прямого доступа из потомка
local function private(self, txt)
print('Hello from ' .. txt)
end
return {
val = 0,
public = function(self)
private(self, 'public')
end,
}
end)())
Ну тут вариантов много. Меня вполне устраивает вариант с соглашением по именованию.
Вот такие возможности предоставляют метатаблицы в Lua. Если вы смогли это все прочитать, то, видимо, написано было не зря.
Полный и чуть более навороченный вариант реализации можно увидеть тут.
Автор: AterCattus