ООП в 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 для таблиц.