5.15. Метатаблицы и метаметоды
Метатаблицы и метаметоды
Поведение таблиц
Переопределение поведения таблиц
Метатаблица
Метаметод
__add, __index, __newindex, __call, __tostring и другие примеры
реализация классов через __index
Аналог полиморфизма, операторной перегрузки.
ООП на основе таблиц и метатаблиц
Нет классов «из коробки» — но можно эмулировать.
Наследование через цепочки метатаблиц.
Сравнение с Ruby/Python: гибкость vs читаемость.
Таблицы используются повсеместно: как массивы, словари, объекты, модули и даже классы. Однако их истинная гибкость проявляется не только в структуре хранения данных, но и в возможности динамического переопределения поведения операций над ними. Именно эту возможность обеспечивают метатаблицы и метаметоды.
Таблица в Lua — это ассоциативный массив, реализованный как хеш-таблица (или комбинация массива и хеша для оптимизации). Она представляет собой коллекцию пар ключ–значение, где ключи и значения могут быть любыми типами, кроме nil. В памяти таблица организована как динамическая структура, поддерживающая эффективные операции вставки, поиска и удаления.
local t = { x = 10, y = 20 }
Это эквивалентно:
local t = {}
t.x = 10
t.y = 20
Внутри интерпретатора каждая таблица содержит:
- Указатель на массив компонентов (для числовых индексов, начиная с 1),
- Хеш-таблицу для произвольных ключей,
- Ссылку на метатаблицу (если она установлена).
Таблицы — единственный составной тип в Lua, способный к изменению поведения через внешние механизмы. Это достигается за счёт метатаблиц.
Метатаблица (metatable) — это обычная таблица, присоединённая к другой таблице (или userdata), которая определяет специальное поведение последней при выполнении определённых операций.
Каждая таблица может иметь не более одной метатаблицы. Метатаблица не наследуется автоматически; она устанавливается явно с помощью функции setmetatable или возвращается функцией getmetatable.
local t = {}
local mt = {}
setmetatable(t, mt)
assert(getmetatable(t) == mt)
Метатаблица действует как описатель поведения: она не хранит данные объекта напрямую, но задаёт правила, по которым объект реагирует на операции, такие как сложение, доступ к полям, вызов как функции и т.д.
Метаметод (metamethod) — это поле внутри метатаблицы, имя которого начинается с двойного подчёркивания (__), и которое определяет поведение при определённой операции.
Например, метаметод __add определяет, что происходит при использовании оператора + с таблицей.
Каждый метаметод соответствует конкретному событию в жизненном цикле операции. Когда интерпретатор сталкивается с операцией над таблицей, он проверяет наличие соответствующего метаметода в её метатаблице и, если он найден, вызывает его вместо стандартного поведения.
При попытке выполнить операцию a + b, если один из операндов — таблица с метатаблицей, содержащей __add, вызывается именно эта функция. Если нет — генерируется ошибка (если оба операнда не являются числами).
Рассмотрим наиболее важные метаметоды и их применение.
__index— перехват чтения отсутствующих полей. Определяет поведение при попытке доступа к несуществующему ключу.- Если
__index— функция, она вызывается с двумя аргументами: таблицей и ключом. - Если
__index— таблица, поиск продолжается в этой таблице.
- Если
local defaults = { color = "white", size = "medium" }
local obj = {}
setmetatable(obj, { __index = defaults })
print(obj.color) -- "white" (берётся из defaults)
Этот механизм лежит в основе делегирования и используется для эмуляции наследования.
2. __newindex — перехват записи в поля. Вызывается при попытке присвоить значение новому (или существующему) ключу.
- Позволяет контролировать, как и куда записываются данные.
- Может использоваться для создания защищённых таблиц, прокси или реактивных систем.
local t = {}
local proxy = {}
setmetatable(proxy, {
__newindex = function(tbl, key, value)
print("Запись:", key, "=", value)
rawset(t, key, value) -- обход метаметода
end
})
proxy.x = 10 -- Вывод: Запись: x = 10
Важно: Для обхода метаметодов используются rawget, rawset, rawlen.
__add,__sub,__mul,__div,__mod,__pow— арифметические метаметоды. Позволяют перегружать арифметические операторы.
local Vec2 = { x = 0, y = 0 }
function Vec2:new(x, y)
local obj = { x = x or 0, y = y or 0 }
setmetatable(obj, self)
self.__index = self
return obj
end
function Vec2:__add(other)
return Vec2:new(self.x + other.x, self.y + other.y)
end
local a = Vec2:new(1, 2)
local b = Vec2:new(3, 4)
local c = a + b
print(c.x, c.y) -- 4 6
Таким образом, Lua поддерживает операторную перегрузку, аналогично C++ или Python.
__call— вызов таблицы как функции. Позволяет использовать таблицу как вызываемый объект.
local Counter = { count = 0 }
function Counter:__call()
self.count = self.count + 1
return self.count
end
setmetatable(Counter, Counter)
print(Counter()) -- 1
print(Counter()) -- 2
Часто применяется для создания фабрик, синглтонов или DSL.
__tostring— строковое представление. Определяет, как таблица конвертируется в строку (например, при print).
function Vec2:__tostring()
return string.format("Vec2(%g, %g)", self.x, self.y)
end
print(a) -- Vec2(1, 2)
Без __tostring print(t) выводит что-то вроде table: 0x....
__eq,__lt,__le— сравнение таблиц. По умолчанию таблицы сравниваются по ссылке. Эти метаметоды позволяют задать логическое равенство или порядок.
function Vec2:__eq(other)
return self.x == other.x and self.y == other.y
end
setmetatable(Vec2, { __eq = Vec2.__eq })
local v1 = Vec2:new(1, 2)
local v2 = Vec2:new(1, 2)
print(v1 == v2) -- true (если метаметод установлен корректно)
Ограничение: __eq работает только при явном сравнении двух таблиц с одним и тем же метаметодом.
Lua не имеет встроенных классов, но предоставляет все средства для эмуляции объектно-ориентированного программирования на основе таблиц и делегирования.
«Класс» в Lua — это таблица, выступающая как прототип и хранилище методов.
Person = {}
Person.__index = Person
function Person:new(name, age)
local instance = setmetatable({}, self)
instance.name = name
instance.age = age
return instance
end
function Person:greet()
print("Привет, меня зовут " .. self.name)
end
Наследование реализуется путём установки родительского прототипа как метатаблицы дочернего класса.
Student = Person:new() -- Student наследует от Person
Student.__index = Student
function Student:new(name, age, grade)
local instance = Person:new(name, age)
setmetatable(instance, self)
instance.grade = grade
return instance
end
local s = Student:new("Анна", 20, "A")
s:greet() -- Привет, меня зовут Анна
Цепочка поиска методов:
- Поле ищется в самой таблице.
- Если не найдено — в
__indexметатаблицы. - Рекурсивно, пока не будет найдено или не завершится цепочка.
Это прототипное наследование, аналогичное JavaScript.
Хотя Lua не поддерживает полиморфизм в классическом смысле (перегрузка функций по сигнатурам), метаметоды обеспечивают поведенческий полиморфизм:
- Один и тот же оператор (+, #, () и т.д.) вызывает разные функции в зависимости от типа операндов.
- Это ад-хок полиморфизм, близкий к тому, что реализован в Python (
__add__) или Ruby (+).
-- Сложение векторов
mt_vec.__add = add_vectors
-- Сложение матриц
mt_mat.__add = add_matrices
-- Оператор + работает полиморфно
a + b -- вызовет нужную реализацию в зависимости от типов
Lua предлагает минималистичный, но мощный механизм, позволяющий строить сложные абстракции поверх простых примитивов. Однако такая гибкость требует дисциплины: без соглашений код может стать трудным для понимания. Метатаблицы и метаметоды — это фундаментальная абстракция, превращающая Lua из простого скриптового языка в мощную платформу для создания доменно-ориентированных языков (DSL), игровых движков, конфигурационных систем и сложных ООП-архитектур.