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
|