Справочное руководство по языку 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