Объекты в Lua

Объекты в Lua

В Lua нет ООП из коробки. Но есть таблицы — и метатаблицы.

Метатаблицы

Что такое метатаблица? Таблица, описывающая таблицу, — но это как-то непонятно. Лучше назвать это свойствами таблицы. Вот как у кнопки в GUI есть свойства вроде x, y, isVisible, onClick, так и у таблицы есть свои свойства. С их помощью можно переопределить, заменить обычное поведение Lua в определённых ситуациях, связанных с таблицей, на своё.

Возьмём следующий код:

local rips = {
  irc = "rip"
}

print(rips.ut) -- nil

Как сейчас работает код?

  1. Мы хотим получить значение поля "ut" таблицы rips.
  2. Lua смотрит в таблицу rips. Поля "ut" там, естественно, не обнаруживает.
  3. Lua возвращает nil.

Это обычное поведение Lua при индексировании таблиц (получению её элемента). Но его можно переопределить. Воспользуемся опцией __index. Она может быть функцией или таблицей. Невероятно полезны оба типа, но рассматривать мы будем табличное значение.

Что это за опция такая? Если там таблица, то поле, которое не найдено в базовой таблице, будет искаться в присвоенной этой опции таблице. Без имён и примера ничего не понятно, я думаю, поэтому:

local rips = {
  irc = "rip"
}

setmetatable(rips, {
  __index = {
    ut = "ripped",
    totoro = "dead"
  }
})

print(rips.ut) -- "ripped"

Что творится? По порядку.

  1. Мы хотим получить значение поля "ut" таблицы rips.
  2. Lua смотрит в таблицу rips. Поля "ut" там, естественно, не обнаруживает.
  3. Lua замечает, что у rips есть метатаблица, а там описана опция __index.
  4. Опция __index — таблица. Поэтому Lua пробует получить значение поля "ut" в ней.
  5. Нашлось! Значение — "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().

  1. Это эквивалентно btn.draw(btn).
  2. Нужно получить значение поля "draw" таблицы btn.
  3. Смотрим в таблицу btn. Там поля такого нет.
  4. Однако попутно видим, что у btn есть метатаблица.
  5. В метатаблице указана опция __index, которая является таблицей buttonMethods.
  6. Тогда пробуем найти поле "draw" в buttonMethods. Находим, как ни странно.
  7. Значение является функцией, её возвращаем.
  8. Вызываем эту функцию, передавая первым аргументом таблицу btn.

Таким же образом работают и последующие строки.

Почему мы используем метатаблицы?

  • Это удобно. Если нужно будет добавить одну функцию для всех экземпляров, достаточно просто дописать её в табличку с методами.
  • Это просто. Не нужно писать никаких сложных обработок. Все методы добавляются к объекту одной строчкой.
  • Мы не повторяем одинаковый функционал для каждого объекта. Соблюдается DRY (don't repeat yourself). Сохраняется память.

Красота же. Сразу находится одна проблемка: у нас засоряется окружение всякими таблицами с методами. Но тут достаточно просто вынести её в отдельную область видимости с помощью конструкции 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

Я ещё воспользовался объявлением методов с двоеточием, к слову. Разнообразия ради.

Для небольших программок такой принцип создания объектов сойдёт. Когда же потребуется наследование (или даже миксины), лучше не изобретать велосипед, а использовать полноценную ООП-библиотеку. На этой странице есть их списочек с основными фичами.

Подробнее

results for ""

    No results matching ""