Перейти к основному содержимому

Метатаблицы и метаметоды

Разработчику Архитектору

Метатаблицы и метаметоды

Пример таблицы

Код ITЗагрузка примера кода…

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • Оператор .. склеивает строки (и автоматически приводит числа к строке при конкатенации).
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Таблица Unit служит шаблоном для создания объектов. В Lua таблицы представляют универсальную структуру данных, способную хранить пары ключ-значение и реализовывать объектное поведение через метатаблицы.

Метод new создаёт новую таблицу instance с начальными значениями полей. Функция setmetatable устанавливает метатаблицу для экземпляра, связывая его с таблицей Unit. Поле __index в метатаблице обеспечивает делегирование: при обращении к отсутствующему ключу в экземпляре поиск продолжается в таблице класса. Конструктор возвращает готовый объект с установленной метатаблицей.

Синтаксис function Unit:damage() эквивалентен function Unit.damage(self). Двоеточие автоматически передаёт первый параметр self, ссылающийся на объект, у которого вызывается метод. Такой подход упрощает вызов методов и делает код более читаемым.

Метод damage обращается к полям объекта через self.поле. Выражение складывает характеристики персонажа и добавляет бонус уровня. Каждый вызов метода производит актуальный расчёт без сохранения промежуточного значения.

Метод attack принимает целевой объект в параметре target. Локальная переменная dmg сохраняет результат вызова self:damage() для повторного использования. Оператор конкатенации .. объединяет строки и числовые значения в единое сообщение. Оператор присваивания с вычитанием уменьшает здоровье цели. Функция print выводит сообщение в консоль с автоматическим переносом строки.

Вызов Unit:new() создаёт новый объект с начальными значениями. Ключевое слово local ограничивает область видимости переменной текущим блоком кода. После создания объекта значения его полей изменяются через прямое присваивание. Каждый объект хранит собственный набор значений независимо от других экземпляров.

Синтаксис warrior:attack(mage) передаёт объект warrior как параметр self в метод attack, а объект mage как параметр target. Такой вызов обеспечивает доступ к полям и методам обоих объектов внутри тела метода. Последовательные вызовы демонстрируют изменение состояния объектов при взаимодействии.

Метатаблица с полем __index реализует прототипное наследование. При обращении к несуществующему полю в экземпляре интерпретатор автоматически проверяет таблицу, указанную в __index. Такой механизм позволяет всем экземплярам разделять одни и те же методы, хранящиеся в таблице класса, без дублирования кода.

Lua использует динамическую типизацию. Переменные не имеют фиксированного типа, тип значения определяется во время выполнения. Числовые значения автоматически преобразуются в строки при конкатенации. Отсутствие строгой типизации упрощает написание кода, но требует внимательности при операциях с разнотипными данными.

Интерпретатор Lua последовательно выполняет инструкции файла. Создаются два объекта с различными характеристиками. Первый вызов метода уменьшает здоровье мага на величину урона воина. Второй вызов метода уменьшает здоровье воина с учётом его текущих характеристик. Каждое действие сопровождается выводом информационного сообщения, отражающего изменение состояния объектов.


Таблицы - местный ООП

Таблицы используются повсеместно — как массивы, словари, объекты, модули и даже классы. Однако их истинная гибкость проявляется не только в структуре хранения данных, но и в возможности динамического переопределения поведения операций над ними. Именно эту возможность обеспечивают метатаблицы и метаметоды.

Таблица в Lua — это ассоциативный массив, реализованный как хеш-таблица (или комбинация массива и хеша для оптимизации). Она представляет собой коллекцию пар ключ–значение, где ключи и значения могут быть любыми типами, кроме nil. В памяти таблица организована как динамическая структура, поддерживающая эффективные операции вставки, поиска и удаления.

local t = { x = 10, y = 20 }

Разбор:

  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Это эквивалентно:

local t = {}
t.x = 10
t.y = 20

Разбор:

  • Код выполняется построчно: каждая инструкция использует значения, созданные строками выше.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Внутри интерпретатора каждая таблица содержит:

  • Указатель на массив компонентов (для числовых индексов, начиная с 1),
  • Хеш-таблицу для произвольных ключей,
  • Ссылку на метатаблицу (если она установлена).

Таблицы — единственный составной тип в Lua, способный к изменению поведения через внешние механизмы. Это достигается за счёт метатаблиц.


Метатаблица

Метатаблица (metatable) — это обычная таблица, присоединённая к другой таблице (или userdata), которая определяет специальное поведение последней при выполнении определённых операций.

Каждая таблица может иметь не более одной метатаблицы. Метатаблица не наследуется автоматически; она устанавливается явно с помощью функции setmetatable или возвращается функцией getmetatable.

local t = {}
local mt = {}
setmetatable(t, mt)
assert(getmetatable(t) == mt)

Разбор:

  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Ключевые вызовы в фрагменте — assert(), getmetatable(), setmetatable().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Метатаблица действует как описатель поведения — она не хранит данные объекта напрямую, но задаёт правила, по которым объект реагирует на операции, такие как сложение, доступ к полям, вызов как функции и т.д.

Кто может иметь свою метатаблицу

Тип значенияМетатаблица
table, userdataсвоя на каждый экземпляр (setmetatable / getmetatable из Lua)
string, number, boolean и др.одна общая метатаблица на тип (меняется из C, не из обычного скрипта)

Новая таблица по умолчанию без метатаблицы (nil). Поля метатаблицы с именами __…метаметоды: если операция для операнда не определена, VM ищет соответствующий метаметод и вызывает его. Так реализуют арифметику "своих" типов, наследование через __index, слабые таблицы (__mode), финализаторы (__gc) и сахар __tostring в библиотеке string.

local mt = {}
mt.__add = function(a, b)
local res = {}
for k in pairs(a) do res[k] = a[k] + b[k] end
return res
end
setmetatable(t1, mt)
local t3 = t1 + t2 -- вызов __add

Метаметод

Метаметод (metamethod) — это поле внутри метатаблицы, имя которого начинается с двойного подчёркивания (__), и которое определяет поведение при определённой операции. Например, метаметод __add определяет, что происходит при использовании оператора + с таблицей.

Каждый метаметод соответствует конкретному событию в жизненном цикле операции. Когда интерпретатор сталкивается с операцией над таблицей, он проверяет наличие соответствующего метаметода в её метатаблице и, если он найден, вызывает его вместо стандартного поведения.

При попытке выполнить операцию a + b, если один из операндов — таблица с метатаблицей, содержащей __add, вызывается именно эта функция. Если нет — генерируется ошибка (если оба операнда не являются числами).


Наиболее важные метаметоды

Рассмотрим наиболее важные метаметоды и их применение.


__index

__index — перехват чтения отсутствующих полей. Определяет поведение при попытке доступа к несуществующему ключу.

  • Если __index — функция, она вызывается с двумя аргументами: таблицей и ключом.
  • Если __index — таблица, поиск продолжается в этой таблице.
local defaults = { color = "white", size = "medium" }
local obj = {}
setmetatable(obj, { __index = defaults })

print(obj.color) -- "white" (берётся из defaults)

Разбор:

  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Этот механизм лежит в основе делегирования и используется для эмуляции наследования.


__newindex

__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

Разбор:

  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте — print(), rawset(), setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Важно — Для обхода метаметодов используются rawget, rawset, rawlen.


Арифметические метаметоды

__add, __sub, __mul, __div, __mod, __pow — арифметические метаметоды.

Позволяют перегружать арифметические операторы.

Код ITЗагрузка примера кода…

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • Логика записывается словами and/or/not, а не символами &&/||/! как в C-подобных языках.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Таким образом, Lua поддерживает операторную перегрузку, аналогично C++ или Python.


__call

__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

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Часто применяется для создания фабрик, синглтонов или DSL.


__tostring

__tostring — строковое представление. Определяет, как таблица конвертируется в строку (например, при print).

function Vec2:__tostring()
return string.format("Vec2(%g, %g)", self.x, self.y)
end

print(a) -- Vec2(1, 2)

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • Вызовы из стандартных библиотек string — готовые функции языка, не нужно писать их с нуля.
  • Ключевые вызовы в фрагменте: print(), string.format().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Без __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 (если метаметод установлен корректно)

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • Логика записывается словами and/or/not, а не символами &&/||/! как в C-подобных языках.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Ограничение: __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

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • Оператор .. склеивает строки (и автоматически приводит числа к строке при конкатенации).
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Наследование реализуется путём установки родительского прототипа как метатаблицы дочернего класса (не экземпляра):

Student = setmetatable({}, { __index = Person })
Student.__index = Student

function Student:new(name, age, grade)
local instance = setmetatable({}, self)
instance.name = name
instance.age = age
instance.grade = grade
return instance
end

local s = Student:new("Анна", 20, "A")
s:greet() --> Привет, меня зовут Анна

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Синтаксис obj:method() передаёт obj первым аргументом (self) — удобный стиль для методов таблиц.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Цепочка поиска методов:

  • Поле ищется в самой таблице.
  • Если не найдено — в __index метатаблицы.
  • Рекурсивно, пока не будет найдено или не завершится цепочка.

Это прототипное наследование, аналогичное JavaScript.

Хотя Lua не поддерживает полиморфизм в классическом смысле (перегрузка функций по сигнатурам), метаметоды обеспечивают поведенческий полиморфизм:

  • Один и тот же оператор (+, #, () и т.д.) вызывает разные функции в зависимости от типа операндов.
  • Это ад-хок полиморфизм, близкий к тому, что реализован в Python (__add__) или Ruby (+).
-- Сложение векторов
mt_vec.__add = add_vectors

-- Сложение матриц
mt_mat.__add = add_matrices

-- Оператор + работает полиморфно
a + b -- вызовет нужную реализацию в зависимости от типов

Разбор:

  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Lua предлагает минималистичный, но мощный механизм, позволяющий строить сложные абстракции поверх простых примитивов. Однако такая гибкость требует дисциплины: без соглашений код может стать трудным для понимания. Метатаблицы и метаметоды — это фундаментальная абстракция, превращающая Lua из простого скриптового языка в мощную платформу для создания доменно-ориентированных языков (DSL), игровых движков, конфигурационных систем и сложных ООП-архитектур.