В Lua нет ООП из коробки. Но есть таблицы — и метатаблицы.
Что такое метатаблица? Таблица, описывающая таблицу, — но это как-то непонятно.
Лучше назвать это свойствами таблицы. Вот как у кнопки в GUI есть свойства вроде
x
, y
, isVisible
, onClick
, так и у таблицы есть свои свойства. С их
помощью можно переопределить, заменить обычное поведение Lua в определённых
ситуациях, связанных с таблицей, на своё.
Возьмём следующий код:
local rips = {
irc = "rip"
}
print(rips.ut) -- nil
Как сейчас работает код?
"ut"
таблицы rips
.rips
. Поля "ut"
там, естественно, не обнаруживает.nil
.Это обычное поведение Lua при индексировании таблиц (получению её элемента).
Но его можно переопределить. Воспользуемся опцией __index
. Она может быть
функцией или таблицей. Невероятно полезны оба типа, но рассматривать мы будем
табличное значение.
Что это за опция такая? Если там таблица, то поле, которое не найдено в базовой таблице, будет искаться в присвоенной этой опции таблице. Без имён и примера ничего не понятно, я думаю, поэтому:
local rips = {
irc = "rip"
}
setmetatable(rips, {
__index = {
ut = "ripped",
totoro = "dead"
}
})
print(rips.ut) -- "ripped"
Что творится? По порядку.
"ut"
таблицы rips
.rips
. Поля "ut"
там, естественно, не обнаруживает.rips
есть метатаблица, а там описана опция __index
.__index
— таблица. Поэтому Lua пробует получить значение поля
"ut"
в ней."ripped"
. Его Lua и возвращает, наконец.Смотрите, как поменялось исполнение. И это всего лишь после указывания опции
__index
таблице.
Как можно было догадаться, setmetatable
первым аргументом берёт исходную
таблицу, и затем прицепляет к ней все опции, описанные в втором аргументе.
Она изменяет сразу исходную таблицу, а не создаёт новую. И возвращаемое значение
у функции — это исходная таблица. Следующий отрывок абсолютно эквивалентен тому,
что выше:
local rips = {
irc = "rip"
}
rips = setmetatable(rips, {
__index = {
ut = "ripped",
totoro = "dead"
}
})
print(rips.ut)
Ещё одна фишка, которая невероятно часто используется с псевдо-ООП в Lua — это синтаксис вызова метода. Вы когда-нибудь замечали в коде что-то вроде такого?
tbl:method("rip")
Обратите внимание на двоеточие, которое было использовано вместо точки. Его можно использовать при вызове метода и объявлении функции. Это просто синтаксический сахар, который автоматически первым аргументом методу передаёт таблицу до двоеточия. Пример выше раскрывается так:
tbl.method(tbl, "rip")
Если функция является методом (то есть вызываться через двоеточие должна), то её можно объявить вот так:
function tbl:method(rip)
-- код метода
print(self, rip)
end
Между таблицей и названием метода двоеточие. Такое объявление автоматически,
неявно первым параметром указывает self
. Вот такое делает:
function tbl.method(self, rip)
-- код метода
print(self, rip)
end
self
для Lua — это такое же название переменной, как и любое другое. Просто
так сложилось, что параметр, указывающий на собственный объект, называется
self
. Его можно указывать самому, как во втором случае, а можно сокращённо,
неявно, как в первом.
Естественно, если хотите сделать метод сразу в объявлении таблицы, то параметр
self
придётся указывать самому:
local tbl = {
method = function(self, rip)
-- код метода
print(self, rip)
end
}
Наконец, настало время объединить всё воедино и создать функцию, которая клепает объекты. Я где-то упоминал кнопку GUI, поэтому её подобие и сделаем.
local buttonMethods = {
getX = function(self)
return self.x
end,
getY = function(self)
return self.y
end,
getBackground = function(self)
return self.background
end,
setBackground = function(self, color)
self.background = color
end,
draw = function(self)
-- нарисовать кнопку
end
}
local function createButton(x, y, w, h)
local obj = {
x = x,
y = y,
w = w,
h = h,
background = 0x000000,
foreground = 0xffffff,
textAlign = "center",
text = "button",
catch = false,
type = "button"
}
return setmetatable(obj, {__index = buttonMethods})
end
local btn = createButton(1, 1, 10, 1)
btn:draw()
btn:setBackground(0x20AFFF)
print(btn:getBackground()) -- 0x20AFFF
В таблицу obj
запихиваем только свойства, которые должны быть уникальны для
каждого нового объекта, соответственно. А в buttonMethods
— общее для всех.
Давайте тогда разберёмся со строкой btn:draw()
.
btn.draw(btn)
."draw"
таблицы btn
.btn
. Там поля такого нет.btn
есть метатаблица.__index
, которая является таблицей
buttonMethods
."draw"
в buttonMethods
. Находим, как ни странно.btn
.Таким же образом работают и последующие строки.
Почему мы используем метатаблицы?
Красота же. Сразу находится одна проблемка: у нас засоряется окружение всякими таблицами с методами. Но тут достаточно просто вынести её в отдельную область видимости с помощью конструкции do/end. Вот так, например:
-- - Объявляем локальную переменную createButton. Значение мы присвоим
-- уже внутри блока do/end.
-- - `do` можно было вынести и на следующую строчку, но так прикольнее
local createButton do
local buttonMethods = {}
function buttonMethods:getX()
return self.x
end
function buttonMethods:getY()
return self.y
end
function buttonMethods:getBackground()
return self.background
end
function buttonMethods:setBackground(color)
self.background = color
end
function buttonMethods:draw(self)
-- нарисовать кнопку
end
-- Заметьте, что тут нет `local`.
-- Однако `createButton` не станет глобальной, так как мы объявили
-- эту переменную локальной в области видимости выше, в самом начале.
function createButton(x, y, w, h)
local obj = {
x = x,
y = y,
w = w,
h = h,
background = 0x000000,
foreground = 0xffffff,
textAlign = "center",
text = "button",
catch = false,
type = "button"
}
return setmetatable(obj, {__index = buttonMethods})
end
end
local btn = createButton(1, 1, 10, 1)
btn:draw()
btn:setBackground(0x20AFFF)
print(btn:getBackground()) -- 0x20AFFF
Я ещё воспользовался объявлением методов с двоеточием, к слову. Разнообразия ради.
Для небольших программок такой принцип создания объектов сойдёт. Когда же потребуется наследование (или даже миксины), лучше не изобретать велосипед, а использовать полноценную ООП-библиотеку. На этой странице есть их списочек с основными фичами.