Metatables в Lua: просто о сложном

Разберём, что такое metatables в Lua, зачем они нужны и как с их помощью добавить таблицам «суперспособности».

LuaРазработка

6 мин

Мы часто хотим, чтобы таблицы вели себя «по‑особенному»: складывались как векторы, подставляли значения по умолчанию, красиво печатались. ✨ Для этого в Lua есть metatables — скрытые сценаристы поведения таблиц. Ниже — краткий, практичный и безболезненный разбор.

🧩 Что такое metatables

Metatable — это обычная таблица, прикреплённая к другой таблице через setmetatable(), которая описывает, как объект реагирует на операции: индексирование, сложение, сравнение, конкатенацию, вызов и т. д.

Думай о метатаблице как о «слое правил»: Lua проверяет ключи и операции — и, если находит соответствующий метаметод (например, __index), использует его.

🧰 Зачем это нужно

Задача

Метаметод

Значения по умолчанию / прототип

__index

Контроль/запрет записи

__newindex

Переопределение сложения/умножения/…

__add, __mul, …

Красивый вывод

__tostring

Сравнение / длина / конкатенация

__eq, __lt, __len, __concat

Делаем таблицу «вызываемой»

__call

Защищаем метатаблицу

__metatable

Самый короткий пример: сложение таблиц как векторов

local mt = {
  __add = function(a, b)
    return { a[1] + b[1], a[2] + b[2] }
  end
}

local v1 = setmetatable({1, 2}, mt)
local v2 = setmetatable({3, 4}, mt)

local sum = v1 + v2
print(sum[1], sum[2]) --> 4  6

Без метатаблиц v1 + v2 вызвало бы ошибку: Lua не знает, как складывать таблицы.

🧭 __index: значения по умолчанию и «наследование»

__index срабатывает, когда ключа в таблице нет. Его можно задать ссылкой на таблицу‑прототип или функцией.

Вариант 1: прототип

local defaults = { speed = 10, hp = 100 }
local player = setmetatable({}, { __index = defaults })

print(player.speed)  --> 10 (берётся из defaults)
player.speed = 20    -- теперь свой speed у player
print(player.speed)  --> 20

Вариант 2: автосоздание значений (ленивая инициализация)

local counts = setmetatable({}, {
  __index = function(t, key)
    local v = 0
    rawset(t, key, v)     -- записываем сразу, чтобы дальше было «как будто существовало всегда»
    return v
  end
})

counts["apples"] = counts["apples"] + 1
print(counts["apples"]) --> 1

Используем rawset, чтобы не вызывать __newindex и не попасть в рекурсию.

🛡️ __newindex: контроль записи

local guarded = setmetatable({ x = 0, y = 0 }, {
  __newindex = function(t, k, v)
    if k == "x" or k == "y" then
      rawset(t, k, v)              -- безопасная запись без повторного вызова __newindex
    else
      error("Нельзя добавлять новое поле: " .. tostring(k), 2)
    end
  end
})

guarded.x = 10    -- ок
guarded.title = "oops"  -- ошибка

Частая ошибка — внутри __newindex написать t[k] = v. Это снова вызовет __newindex и приведёт к рекурсии. Используйте rawset.

⚠️ Подводные камни

  • Метатаблицы не копируются при поверхностном копировании таблицы — навешивайте заново.

  • Слишком хитрые цепочки __index усложняют отладку. Держите модель простой.

  • rawget/rawset обходят метаметоды — это и сила, и риск. Используйте осознанно.

🚀 Продвинутые приёмы Metatables

📞 __call: делаем «фабрику» объектов

Метаметод __call позволяет вызывать таблицу как функцию — удобно для фабрик и DSL.

local greeter = setmetatable({}, {
  __call = function(_, name) print("Привет, " .. name .. "!") end
})

greeter("Кодик") --> Привет, Кодик!

🔒 __metatable: защита от изменений

Спрятать реальные метаметоды и запретить их менять можно так:

local obj = {}
local mt  = { __metatable = "Доступ к метатаблице запрещён" }
setmetatable(obj, mt)

print(getmetatable(obj)) --> Доступ к метатаблице запрещён
-- getmetatable(obj).__index  -- уже не получить

🎨 Красивый вывод и «магические» операторы

local mt = {
  __tostring = function(t) return ("Point(%d, %d)"):format(t.x, t.y) end,
  __len      = function(t) return math.sqrt(t.x*t.x + t.y*t.y) end, -- длина вектора через #
  __eq       = function(a,b) return a.x==b.x and a.y==b.y end,
  __add      = function(a,b) return setmetatable({x=a.x+b.x, y=a.y+b.y}, mt) end,
}
local p = setmetatable({x=3, y=4}, mt)
print(p)    --> Point(3, 4)
print(#p)   --> 5

💬 Где бы ты применил metatables у себя — в игре на Roblox/Love2D, в конфиг‑системе или в мини‑DSL?

📚 Хочешь углубиться в тему?

В приложении Кодик ты найдёшь подробные уроки по Lua, пошаговые упражнения, разбор ошибок и удобную практику прямо в телефоне или браузере.

А если хочешь быть в курсе новостей, новых фич и полезных материалов — подписывайся на наш Telegram-канал. Там уютно, по делу и с любовью к коду ❤️

Комментарии