Метатаблица в Lua

В таблице Lua мы можем обратиться к соответствующему ключу, чтобы получить значение, но не можем выполнять операции над двумя таблицами (например, сложение).

Поэтому Lua предоставляет метатаблицы, позволяя нам изменять поведение таблиц. Каждое поведение связано с соответствующим метаметодом.

Например, используя метатаблицы, мы можем определить, как Lua вычисляет операцию сложения a+b двух таблиц.

Когда Lua пытается сложить две таблицы, она сначала проверяет, есть ли у любой из них метатаблица, затем проверяет, существует ли поле с именем__addЕсли найдено, вызывается соответствующее значение.__addТакие поля; соответствующие им значения (часто функция или таблица) — это "метаметоды".

Существуют две очень важные функции для работы с метатаблицами:

  • setmetatable(table,metatable):Устанавливает метатаблицу для указанной таблицы. Если метатаблица содержит ключ __metatable, setmetatable завершится ошибкой.
  • getmetatable(table):Возвращает метатаблицу объекта.

Следующий пример демонстрирует, как задать метатаблицу для указанной таблицы:

mytable = {}                          -- Обычная таблица
mymetatable = {}                      -- Метатаблица
setmetatable(mytable,mymetatable)     -- Сделать mymetatable метатаблицей mytable

Приведённый выше код также можно записать в одну строку:

mytable = setmetatable({},{})

Следующее возвращает метатаблицу объекта:

getmetatable(mytable)                 -- 这会返回 mymetatable

__index — метаметод

Это наиболее часто используемый ключ в метатаблице.

Когда вы обращаетесь к таблице по ключу, если у ключа нет значения, Lua будет искать ключ __index в метатаблице таблицы (при условии, что метатаблица существует). Если __index содержит таблицу, Lua ищет соответствующий ключ в этой таблице.

Мы можем использовать команду lua, чтобы войти в интерактивный режим и проверить:

$ lua
Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio
> other = { foo = 3 }
> t = setmetatable({}, { __index = other })
> t.foo
3
> t.bar
nil

Если __index содержит функцию, Lua вызовет эту функцию, передавая таблицу и ключ в качестве аргументов функции.

Метаметод __index проверяет, существует ли элемент в таблице. Если его нет, возвращаемый результат — nil; если он есть, результат возвращается с помощью __index.

Пример

mytable = setmetatable({key1 = "value1"}, {
  __index = function(mytable, key)
    if key == "key2" then
      return "metatablevalue"
    else
      return nil
    end
  end
})

print(mytable.key1,mytable.key2)

Вывод примера:

value1    metatablevalue

Анализ примера:

  • Таблица mytable задана как{key1 = "value1"}。

  • У mytable установлена метатаблица, а метаметод — __index.

  • Ищите key1 в таблице mytable. Если найден, верните элемент; если не найден, продолжите.

  • Ищите key2 в таблице mytable. Если найден, верните metatablevalue; если не найден, продолжите.

  • Определите, есть ли у метатаблицы метод __index. Если метод __index является функцией, вызовите эту функцию.

  • В метаметоде проверьте, передан ли параметр ключа "key2" (mytable.key2 установлен). Если параметр "key2" передан, верните "metatablevalue"; в противном случае верните соответствующее значение mytable для этого ключа.

Мы можем переписать приведённый выше код проще так:

mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)

Итог

Правила, по которым Lua ищет элемент таблицы, на самом деле состоят из следующих 3 шагов:

  • 1. Посмотрите в таблице. Если найден, верните элемент; если не найден, продолжите.
  • 2. Определите, есть ли у таблицы метатаблица. Если у неё нет метатаблицы, верните nil; если метатаблица есть, продолжите.
  • 3. Определите, есть ли у метатаблицы метод __index. Если метод __index равен nil, верните nil; если метод __index является таблицей, повторите шаги 1, 2 и 3; если метод __index является функцией, верните возвращаемое значение этой функции.

Эта часть взята у автора Huanzi: https://blog.csdn.net/xocoder/article/details/9028347


Метамеметод __newindex

Метамеметод __newindex используется для обновления таблиц, а __index — для доступа к таблицам.

Когда вы присваиваете значение отсутствующему индексу таблицы, интерпретатор ищет метаметод __newindex: если он существует, он вызывает эту функцию, не выполняя присваивание.

Следующий пример демонстрирует применение метаметода __newindex:

Пример

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1)

mytable.newkey = "new value 2"
print(mytable.newkey,mymetatable.newkey)

mytable.key1 = "new value 1"
print(mytable.key1,mymetatable.key1)

Вывод после выполнения приведённого выше примера:

value1
nil    新值2
新值1    nil

В приведенном выше примере для таблицы установлен метаметод __newindex. При присваивании по новому ключу индекса (newkey) (mytable.newkey = "new value 2") вызывается метаметод, и присваивание не выполняется. При присваивании по существующему ключу индекса (key1) присваивание выполняется, и метаметод __newindex не вызывается.

В следующем примере используется функция rawset для обновления таблицы:

Пример

mytable = setmetatable({key1 = "value1"}, {
    __newindex = function(mytable, key, value)
        rawset(mytable, key, "\""..value.."\"")
    end
})

mytable.key1 = "new value"
mytable.key2 = 4

print(mytable.key1,mytable.key2)

Вывод после выполнения приведённого выше примера:

new value    "4"

Добавление операторов к таблицам

В следующем примере демонстрируется сложение двух таблиц:

Пример

-- Пользовательская функция table_maxn для вычисления максимального значения ключа в таблице, т.е. возвращает максимальное значение ключа таблицы
function table_maxn(t)
    local mn = 0
    for k, _ in pairs(t) do
        if type(k) == "number" and k > mn then
            mn = k
        end
    end
    return mn
end

-- Операция сложения двух таблиц
mytable = setmetatable({ 1, 2, 3 }, {
  __add = function(mytable, newtable)
    local max_key_mytable = table_maxn(mytable)
    for i = 1, table_maxn(newtable) do
      table.insert(mytable, max_key_mytable + i, newtable[i])
    end
    return mytable
  end
})

secondtable = {4, 5, 6}

mytable = mytable + secondtable

for k, v in ipairs(mytable) do
    print(k, v)
end

Вывод после выполнения приведённого выше примера:

1    1
2    2
3    3
4    4
5    5
6    6

Ключ __add включён в метатаблицу и выполняет операцию сложения. Список соответствующих операций в таблице следующий: (Примечание:__это два символа подчёркивания)

ПаттернОписание
__addСоответствующий оператор '+'.
__subСоответствующий оператор '-'.
__mulСоответствующий оператор '*'.
__divСоответствующий оператор '/'.
__modСоответствующий оператор '%'.
__unmСоответствующий оператор '-'.
__concatСоответствующий оператор '..'.
__eqСоответствующий оператор '=='.
__ltСоответствующий оператор '<'.
__leСоответствующий оператор '<='.

метаметод __call

Метаметод __call вызывается, когда Lua вызывает значение. Следующий пример демонстрирует вычисление суммы элементов в таблице:

Пример

-- Вычислить максимальное значение в таблице; table.maxn нельзя использовать в версиях Lua выше 5.2
-- Пользовательская функция table_maxn для вычисления максимального значения ключа в таблице, то есть вычисления количества элементов в таблице
function table_maxn(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- Определение метаметода __call
mytable = setmetatable({10}, {
  __call = function(mytable, newtable)
        sum = 0
        for i = 1, table_maxn(mytable) do
                sum = sum + mytable[i]
        end
    for i = 1, table_maxn(newtable) do
                sum = sum + newtable[i]
        end
        return sum
  end
})
newtable = {10,20,30}
print(mytable(newtable))

Вывод после выполнения приведённого выше примера:

70

метаметод __tostring

Метаметод __tostring используется для изменения поведения вывода таблицы. В следующем примере мы настраиваем выводимое содержимое таблицы:

Пример

mytable = setmetatable({ 10, 20, 30 }, {
  __tostring = function(mytable)
    sum = 0
    for k, v in pairs(mytable) do
                sum = sum + v
        end
    return "Сумма всех элементов в таблице равна " .. sum
  end
})
print(mytable)

Вывод после выполнения приведённого выше примера:

表所有元素的和为 60

Из этой статьи мы можем узнать, что метатаблицы могут эффективно упростить функциональность нашего кода, поэтому понимание метатаблиц Lua может помочь нам писать более простой и лучший код на Lua.

Другие расширения