Справочное руководство по языку Lua 5.1 :: 2.8 - Значения и типы
2.8 - Метатаблицы
Любое значение в Lua может иметь метатаблицу. Метатаблица – это обычная таблица Lua, в которой определены допустимые операции над значением. Вы можете изменить действие некоторых операции над значением путем задания соответствующих полей в его метатаблице. Для экземпляров классов, например когда к нечисловым значениям применяется операция сложения, Lua ищет реализацию этой операции в поле "__add" его метатаблицы. Если реализация найдена, Lua запускает эту операцию для выполнения сложения.
Ключевые поля в метатаблице мы называем событиями, а значения - метаметодами. В рассмотреном примере событием является "add", а метаметодом является функция сложения.
Получить метатаблицу любого значения можно с помощью функции getmetatable.
Вы можете заменить метатаблицу с помощью функции setmetatable. Вы не можете изменить метатаблицу другим способом (of other types – других типов???) (за исключением случаев использования отладочной библиотеки), для этого требуется воспользоваться C API.
Таблицы и пользовательские данные имеют индивидуальные метатаблицы (в то же время множества таблиц и пользовательских данных может совместно использовать соответствующие метатаблицы). Переменные всех других типов совместно используют одну метатаблицу на тип. То есть есть одна метатаблица на все числовые значения, одна на все строки и т.д.
В метатаблице могут быть заданы правила выполнения арифметических операций над объектом, порядок сравнения, конкатенации, способ вычисления длины и индексирования. Также в метатаблице может быть определена функция «сборки мусора». Для каждой из этих операций в Lua определен специальный ключ, называемый событие. В момент, когда Lua выполняет одну из этих операций над значением, проверяется, есть ли в метатаблице значение с соответствующим событием. Если оно найдено, значение по этому ключу (метаметод) и определяет способ выполнения операции.
Далее рассмотрим управление операциями с помощью метатаблиц. Каждая операция идентифицируется по своему имени. Ключ каждой операции представляет из себя строку из имени и двух нижних подчеркиваний перед ним. Для экземпляра – ключом операции “add” будет строка "__add". Для лучшего понимания мы в терминах Lua покажем запуск операции интерпретатором.
Представленный здесь код на Lua приведен в качестве иллюстрации, реальный код интерпретатора гораздо сложнее и эффективнее этого схематичного примера. Все функции, использованные в этом примере (rawget, tonumber и т.п.), описаны в §5.1. Вообще говоря, для получения метаметода объекта мы используем конструкцию
metatable(obj)[event]
Это следует читать как
rawget(getmetatable(obj) or {}, event)
Таким образом, при получении доступа к метаметоду другие метаметоды не используются, поэтому доступ к объекту без метатаблицы не приводит к ошибке (мы просто получим nil).
· "add": операция сложения.
Функция getbinhandler ниже показывает, каким образом Lua получает указатель на операцию. Во-первых, Lua пытается получить первый операнд. Если его тип не является типом указателя на операцию, Lua пытается получить второй операнд.
function getbinhandler (op1, op2, event)
return metatable(op1)[event] or metatable(op2)[event]
end
С помощью этой функции поведение конструкции op1 + op2 выглядит как
function add_event (op1, op2)
local o1, o2 = tonumber(op1), tonumber(op2)
if o1 and o2 then -- операнды являются числами?
return o1 + o2 -- '+' здесь – стандартная операция сложения
else -- когда хотя бы один из операндов нечисловой
local h = getbinhandler(op1, op2, "__add")
if h then
-- вызов функции с операндами по указателю
return h(op1, op2)
else -- указатель не найден: обработка ошибки
error(···)
end
end
end
· "sub": операция ‘ - ‘. Обработка аналогична "add".
· "mul": операция ‘ * ’. Обработка аналогична "add".
· "div": операция ‘ / ’ . Обработка аналогична “add”.
· "mod": операция ‘ % ’. Обработка похожа на "add", только o1 - floor(o1/o2)*o2 подставляется вместо стандартной операции.
· "pow": операция ‘ ^ ’. Обработка похожа на "add", вместо стандартной операции подставляется функция pow (из математической библиотеки C).
· "unm": операция «унарный минус».
function unm_event (op)
local o = tonumber(op)
if o then -- операнд является числом?
return -o -- '-' здесь – стандартый оператор
else -- операнд нечисловой.
-- Попытка получить указатель на функцию по операнду
local h = metatable(op).__unm
if h then
-- вызов функции с операндом на входе
return h(op)
else -- указатель не найден: обработка ошибки
error(···)
end
end
end
· "concat": Операция конкатенации ‘..’
function concat_event (op1, op2)
if (type(op1) == "string" or type(op1) == "number") and
(type(op2) == "string" or type(op2) == "number") then
return op1 .. op2 -- стандартная конкатенация строк
else
local h = getbinhandler(op1, op2, "__concat")
if h then
return h(op1, op2)
else
error(···)
end
end
end
· "len": Операция ‘#’
function len_event (op)
if type(op) == "string" then
return strlen(op) -- стандартная функция
elseif type(op) == "table" then
return #op -- стандартная функция для таблиц
else
local h = metatable(op).__len
if h then
-- вызов функции по указателю
return h(op)
else -- указатель не найден: обработка ошибки
error(···)
end
end
end
Смотрите §2.5.5 с описанием функции вычисления длины таблицы.
· " eq ": операция ‘= =’. Функция getcomphandler определяет, как выбирает метаметод для сравнения операторов. Метаметод вызывается, только если сравниваются объекты одного типа, и метаметоды объектов для этой операции равны.
function getcomphandler (op1, op2, event)
if type(op1) ~= type(op2) then return nil end
local mm1 = metatable(op1)[event]
local mm2 = metatable(op2)[event]
if mm1 == mm2 then return mm1 else return nil end
end
Событие "eq" определяется следующим образом:
function eq_event (op1, op2)
if type(op1) ~= type(op2) then -- типы различны?
return false -- объекты не равны
end
if op1 == op2 then -- стандартное сравнение истинно?
return true -- объекты равны
end
-- попытка получения метаметода
local h = getcomphandler(op1, op2, "__eq")
if h then
return h(op1, op2)
else
return false
end
end
a ~= b означает отрицание (a == b).
· "lt": операция ‘<’.
function lt_event (op1, op2)
if type(op1) == "number" and type(op2) == "number" then
return op1 < op2 -- числовое сравнение
elseif type(op1) == "string" and type(op2) == "string" then
return op1 < op2 -- лексическое сравнение
else
local h = getcomphandler(op1, op2, "__lt")
if h then
return h(op1, op2)
else
error(···);
end
end
end
a > b эквивалентно b < a.
· "le": операция ‘ <= ’.
function le_event (op1, op2)
if type(op1) == "number" and type(op2) == "number" then
return op1 <= op2 -- числовое сравнение
elseif type(op1) == "string" and type(op2) == "string" then
return op1 <= op2 -- лексическое сравнение
else
local h = getcomphandler(op1, op2, "__le")
if h then
return h(op1, op2)
else
h = getcomphandler(op1, op2, "__lt")
if h then
return not h(op2, op1)
else
error(···);
end
end
end
end
a >= b эквивалентно b <= a. Заметим, что при отсутствии метаметода "le" , Lua пытается применить метаметод "lt", полагая, что a <= b эквиваленто отрицанию (b < a.
· "index": Доступ по индексу table[key].
function gettable_event (table, key)
local h
if type(table) == "table" then
local v = rawget(table, key)
if v ~= nil then return v end
h = metatable(table).__index
if h == nil then return nil end
else
h = metatable(table).__index
if h == nil then
error(···);
end
end
if type(h) == "function" then
return h(table, key) -- вызов по указателю
else return h[key] -- или рекурсивный вызов операции
end
end
· "newindex": Индексированное присваивание table[key] = value.
function settable_event (table, key, value)
local h
if type(table) == "table" then
local v = rawget(table, key)
if v ~= nil then rawset(table, key, value); return end
h = metatable(table).__newindex
if h == nil then rawset(table, key, value); return end
else
h = metatable(table).__newindex
if h == nil then
error(···);
end
end
if type(h) == "function" then
return h(table, key,value) -- вызов по указателю
else h[key] = value -- или рекурсивный вызов
end
end
· "call": вызывается, когда Lua запускает функцию.
function function_event (func, ...)
if type(func) == "function" then
return func(...) -- стандартный вызов
else
local h = metatable(func).__call
if h then
return h(func, ...)
else
error(···)
end
end
end