- PVSM.RU - https://www.pvsm.ru -
В 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, но сейчас не про них речь) можно задавать метатаблицу, описывающую поведение данной таблицы в особенных случаях [1]. Такими случаями могут быть использование в арифметике (перегрузка операторов), конкатенация как строк и т.д. В качестве небольшого примера перегрузки операторов и приведения к строке:
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 используется следующим образом:
Такой подход позволяет отделить описание класса от создания его экземпляра:
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() [2] используется для получения краткой информации о запрошенном уровне стека: 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. Если вы смогли это все прочитать, то, видимо, написано было не зря.
Полный и чуть более навороченный вариант реализации можно увидеть тут [3].
Автор: AterCattus
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/lua/63661
Ссылки в тексте:
[1] особенных случаях: http://lua-users.org/wiki/MetatableEvents
[2] debug.getinfo(): http://www.lua.ru/doc/5.9.html
[3] можно увидеть тут: https://github.com/AterCattus/estrela/blob/dev/lib/estrela/oop/single.lua
[4] Источник: http://habrahabr.ru/post/228001/
Нажмите здесь для печати.