ООП в Lua: объекты-таблицы и классы-таблицы, механизм метатаблиц
Опубликовано:
В языке Lua нет встроенного ключевого слова class и привычной из других языков монолитной архитектуры ООП. При этом объектно-ориентированная парадигма программирования в Lua используется широко. Вместо готовых концепций этот язык предоставляет программисту гибкий низкоуровневый конструктор — таблицы и метатаблицы. В этой статье пошагово описывается, как обычная таблица превращается в полноценный объект, наследуемый от таблицы-класса.
Когда в Lua мы с помощью таблиц создаем объект (с технической точки зрения) похожий на структуру в Си или запись в Pascal, то его уже можно рассматривать как data-объект в концепции ООП.
local unit = {name="Rob", hp=100, level=3}
Однако объект-экземпляр в классическом ООП должен хранить не только свои состояния, но и обладать поведением, то есть у него должны быть методы. В Lua можно легко добавить в таблицу функции (в коде ниже показаны три способа):
local unit = {name="Rob", hp=100, level=3, levUp=function (player) player.level = player.level + 1 end} function unit.damageDown (player, amount) player.hp = player.hp - amount end unit.newHP = function (unit) unit.hp = 100 end unit.levUp(unit) unit.damageDown(unit, 30) print(unit.hp, unit.level) -- 70 4
Но добавление функций в их обычном виде не делает их подобием методов, так как в них приходится явно передавать объект, и он может быть любым:
local hero = {nick="Tom", hp=200} unit.newHP(hero) print(hero.hp) -- 100
В Lua есть соглашение об именовании объекта, которому принадлежит метод, через имя self:
local unit = {name="Rob", hp=100, level=3, levUp=function (self) self.level = self.level + 1 end} function unit.damageDown (self, amount) self.hp = self.hp - amount end unit.newHP = function (self) self.hp = 100 end unit.levUp(unit) unit.damageDown(unit, 30) print(unit.hp, unit.level) -- 70 4 local hero = {nick="Tom", hp=200} unit.newHP(hero) print(hero.hp) -- 100
Хотя по существу в листинге выше ничего не поменяло, в Lua есть синтаксический сахар при определении и вызове методов, который работает через двоеточие и предполагает наличие переменной self. В этом случае вызов методов для их объекта-владельца упрощается до объект:метод(аргументы), где в скобках сам объект уже не указывается:
unit:levUp() unit:damageDown(30)
Для сторонних объектов такие вызовы не будут работать. Мы так и будем при вызове чужого метода, передавая в него объект явно.
При определении метода через двоеточие self в заголовке также опускают (в большинстве реализаций Lua это не касается методов, которые задаются при создании таблицы, также нельзя использовать синтаксис описания функции через равно):
local unit = {name="Rob", hp=100, level=3, levUp = function (self) self.level = self.level + 1 end} function unit:damageDown(amount) self.hp = self.hp - amount end function unit:newHP() self.hp = 100 end
Чтобы создавать множество однотипных объектов, обладающих сходным набором полей и методов, то есть когда речь идет классах, используется механизм метатаблиц. Это эмуляция полноценного ООП, так как в Lua нет встроенной концепции классов. Суть имитации заключается в том, что таблицам-записям назначается роль объектов, а методы выносятся в отдельную таблицу, которая играет роль класса. Эта таблица назначается метатаблицей для всех объектов, принадлежащих этому классу. Доступ к её методам настраивается через ее поле __index, которое может указывать как на всю таблицу-класс, так и на определенную часть её содержимого.
Сначала рассмотрим пример, когда все содержимое класса доступно объектам, то есть таблица-класс для них открыта полностью:
local Methods = {levUp = function(self) self.level = self.level + 1 end, damageDown = function(self, amount) self.hp = self.hp - amount end, } Methods.__index = Methods local unit = {name="Rob", hp=100, level=3} local hero = {name="Tom", hp=200, level=10} setmetatable(unit, Methods) setmetatable(hero, Methods) unit:levUp() unit:damageDown(30) print(unit.hp, unit.level) -- 70 4 hero:damageDown(20) print(hero.hp, hero.level) -- 180 10
Выражение Methods.__index = Methods добавляет в таблицу ключ __index, которому присваивается ссылка на саму таблицу. Поле __index для класса не имеет значения, но оно важно для объектов-наследников, которые связываются с ним через вызов функции setmetatable. В результате когда объект не находит метод среди своих собственных, он ищет поле __index в своей метатаблице и как бы наследует то, что содержится в __index. В данном случае — всю таблицу Methods.
Когда ключу __index присваивается сама таблица, в которую он добавляется, то делается это уже после определения таблицы-класса, так как в момент создания таблицы интерпретатор еще не связал ее с переменной-именем.
С другой стороны, значением __index можно быть другая таблица (получается таблица в классе-таблице), в которую заносят только те методы, которые предоставляются объектам (экземплярная часть). Статическая часть (внешняя таблица) доступна только через сам класс.
local Army = { count = 0, __index = { levUp = function(self) self.level = self.level + 1 end, damageDown = function(self, amount) self.hp = self.hp - amount end, } } -- Метод только класса. Здесь self - сам класс function Army:new(nick, hp) self.count = self.count + 1 -- в классе хранится количество воинов local obj = { name = nick, hp = hp, level = 1 } return setmetatable(obj, self) end -- Создаем объект через конструктор класса local hero = Army:new("Tom", 100) hero:damageDown(20) print(hero.hp) -- 80 print(Army.count) -- 1
Функция setmetatable спроектирована в Lua так, что не просто связывает две таблицы, но и возвращает в качестве результата первую таблицу (в примере объект obj). Таким образом, return setmetatable(obj, self) устанавливает метатаблицу для объекта и возвращает его из функции уже связанным с классом.
В Lua поля, которые начинаются с двойного нижнего подчеркивания (например, __index, __newindex, __tostring, __add), называются метаметодами (или метаполями). Они позволяют перегружать стандартное поведение Lua для таблиц.