---
title: "Что такое table и metatable в Lua? - API7.ai"
description: "Table — единственный механизм структурирования данных в Lua. Мы можем использовать таблицы для представления обычных массивов, символьных таблиц, множеств, записей, очередей и других структур данных. Узнайте больше из этой статьи."
url: "https://api7.ai/ru/learning-center/openresty/lua-table-and-metatable"
---

# Что такое table и metatable в Lua?

API7.ai

October 11, 2022

[OpenResty (NGINX + Lua)](https://api7.ai/learning-center/openresty)

Сегодня мы узнаем о единственной структуре данных в LuaJIT: `table`.

В отличие от других языков сценариев с богатыми структурами данных, LuaJIT имеет только одну структуру данных — `table`, которая не разделяется на массивы, хэши, коллекции и т.д., а представляет собой нечто смешанное. Давайте рассмотрим один из примеров, упомянутых ранее.

```
local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"])                 --> output: red
print(color[1])                         --> output: blue
print(color["third"])                --> output: green
print(color[2])                         --> output: yellow
print(color[3])                         --> output: nil
```

В этом примере таблица `color` содержит как массив, так и хэш, и доступ к ним осуществляется без взаимного вмешательства. Например, вы можете использовать функцию `ipairs` для итерации только по массиву таблицы.

```
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
for k, v in ipairs(color) do
      print(k)
end
'
```

Операции с `table` настолько важны, что LuaJIT расширяет стандартную библиотеку таблиц Lua 5.1, а OpenResty расширяет библиотеку таблиц LuaJIT еще дальше. Давайте рассмотрим каждую из этих функций библиотеки.

## Функции библиотеки таблиц

Начнем со стандартных функций библиотеки таблиц. В Lua 5.1 не так много функций библиотеки таблиц, поэтому мы можем быстро их просмотреть.

### `table.getn` Получение количества элементов

Как мы упоминали в главе [Standard Lua and LuaJIT](https://api7.ai/learning-center/openresty/luajit-vs-lua/), получение правильного количества всех элементов таблицы — это большая проблема в LuaJIT.

Для последовательностей вы можете использовать `table.getn` или унарный оператор `#`, чтобы вернуть правильное количество элементов. Следующий пример возвращает ожидаемое число 3.

```
$ resty -e 'local t = { 1, 2, 3 }
  print(table.getn(t))
```

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

```
$ resty -e 'local t = { 1, a = 2 }
  print(#t) '
```

К счастью, такие сложные для понимания функции были заменены расширениями LuaJIT, о которых мы упомянем позже. Поэтому в контексте OpenResty не используйте функцию `table.getn` и унарный оператор `#`, если вы точно не знаете, что получаете длину последовательности.

Кроме того, `table.getn` и унарный оператор `#` имеют временную сложность не O(1), а O(n), что является еще одной причиной избегать их, если это возможно.

### `table.remove` Удаление указанного элемента

Вторая функция — это `table.remove`, которая удаляет элементы в таблице на основе индексов, то есть удаляются только элементы массива таблицы. Давайте снова рассмотрим пример `color`.

```
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
  table.remove(color, 1)
  for k, v in pairs(color) do
      print(v)
  end'
```

Этот код удалит элемент `blue` с индексом 1. Вы можете спросить, как удалить хэш-часть таблицы? Это так же просто, как установить значение, соответствующее ключу, в `nil`. Таким образом, в примере `color`, элемент `green`, соответствующий `third`, будет удален.

```
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
  color.third = nil
  for k, v in pairs(color) do
      print(v)
  end'
```

### `table.concat` Функция объединения элементов

Третья функция — это `table.concat`, функция объединения элементов. Она объединяет элементы таблицы в соответствии с индексами. Поскольку это снова основано на индексах, она работает только с массивной частью таблицы. Снова пример с `color`.

```
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
print(table.concat(color, ", "))'
```

После использования функции `table.concat` вывод будет `blue, yellow`, а хэш-часть будет пропущена.

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

```
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow", "orange"}
print(table.concat(color, ", ", 2, 3))'
```

На этот раз вывод будет `yellow, orange`, пропуская `blue`.

Не стоит недооценивать эту, казалось бы, бесполезную функцию, но она может иметь неожиданные эффекты при оптимизации производительности и является одним из главных героев в наших последующих главах об оптимизации производительности.

### `table.insert` Вставка элемента

Наконец, давайте рассмотрим функцию `table.insert`. Она вставляет новый элемент в указанный индекс, что влияет на массивную часть таблицы. Для иллюстрации снова используем пример `color`.

```
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.insert(color, 1,  "orange")
print(color[1])
'
```

Вы можете видеть, что первый элемент `color` становится `orange`, но, конечно, вы можете не указывать индекс, и тогда элемент будет вставлен в конец очереди по умолчанию.

Следует отметить, что `table.insert` — это распространенная операция, но ее производительность не очень хорошая. Если вы не вставляете элементы на основе указанного индекса, то каждый раз вам нужно будет вызывать `lj_tab_len` LuaJIT, чтобы получить длину массива для вставки в конец очереди. Как и `table.getn`, временная сложность получения длины таблицы составляет O(n).

Поэтому для операции `table.insert` мы должны стараться избегать ее использования в горячем коде. Например:

```
local t = {}
for i = 1, 10000 do
     table.insert(t, i)
end
```

## Расширенные функции таблиц LuaJIT

Далее рассмотрим расширенные функции таблиц LuaJIT. LuaJIT расширяет стандартный Lua двумя полезными функциями для создания и очистки таблиц, которые я опишу ниже.

### `table.new(narray, nhash)` Создание новой таблицы

Первая функция — это `table.new(narray, nhash)`. Вместо того чтобы увеличивать себя при вставке элементов, эта функция предварительно выделяет пространство для указанного массива и хэша, что и означают ее два параметра `narray` и `nhash`. Самоувеличение — это дорогостоящая операция, которая включает выделение пространства, `resize` и `rehash`, и ее следует избегать любой ценой.

Обратите внимание, что документация для `table.new` находится не на сайте LuaJIT, а глубоко в [расширенной документации](https://github.com/openresty/luajit2/blob/v2.1-agentzh/doc/extensions.html) проекта на GitHub, поэтому ее трудно найти даже через Google, и не многие инженеры знают о ней.

Вот простой пример, и я покажу вам, как она работает. Прежде всего, эта функция является расширенной, поэтому перед использованием ее нужно `require`.

```
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
   t[i] = i
end
```

Как видите, этот код создает новую таблицу с 100 элементами массива и 0 элементами хэша. Конечно, вы можете создать новую таблицу с 100 элементами массива и 50 элементами хэша по мере необходимости, что также допустимо.

```
local t = new_tab(100, 50)
```

Или, если вы выйдете за пределы предустановленного размера пространства, вы все равно сможете использовать ее, но производительность ухудшится, и смысл использования `table.new` будет потерян.

В следующем примере у нас предустановленный размер 100, но мы используем 200.

```
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 200 do
   t[i] = i
end
```

Вам нужно предустановить размер массива и хэш-пространства в `table.new` в соответствии с реальным сценарием, чтобы найти баланс между производительностью и использованием памяти.

### `table.clear()` Очистка таблицы

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

```
$ resty -e 'local clear_tab =require "table.clear"
local color = {first = "red", "blue", third = "green", "yellow"}
clear_tab(color)
for k, v in pairs(color) do
     print(k)
end'
```

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

## Расширенные функции таблиц OpenResty

Как я упоминал в начале, OpenResty поддерживает свою собственную ветку LuaJIT, которая также расширяет таблицы, [с несколькими новыми API](https://github.com/openresty/luajit2/#new-api): `table.isempty`, `table.isarray`, `table.nkeys` и `table.clone`.

Перед использованием этих новых API, пожалуйста, проверьте версию OpenResty, так как большинство из них можно использовать только в версиях OpenResty после 1.15.8.1. Это связано с тем, что OpenResty не выпускал новых версий около года до версии 1.15.8.1, и эти API были добавлены в этот промежуток времени.

Я включил ссылку на статью, поэтому я использую `table.nkeys` в качестве примера. Остальные три API легко понять с точки зрения именования, поэтому просмотрите документацию на GitHub, и вы поймете. Должен сказать, что документация OpenResty очень высокого качества, включая примеры кода, можно ли JIT, на что обратить внимание и т.д. На несколько порядков лучше, чем документация Lua и LuaJIT.

Хорошо, вернемся к функции `table.nkeys`. Ее именование может вас запутать, но это функция, которая получает длину таблицы и возвращает количество элементов таблицы, включая элементы массива и хэш-части. Поэтому мы можем использовать ее вместо `table.getn`, например, следующим образом.

```
local nkeys = require "table.nkeys"
print(nkeys({}))  -- 0
print(nkeys({ "a", nil, "b" }))  -- 2
print(nkeys({ dog = 3, cat = 4, bird = nil }))  -- 2
print(nkeys({ "a", dog = 3, cat = 4 }))  -- 3
```

## Метатаблицы

После обсуждения функций таблиц давайте рассмотрим `metatable`, производную от `table`. Метатаблица — это уникальная концепция в Lua, которая широко используется в реальных проектах. Не будет преувеличением сказать, что вы можете найти ее практически в любой библиотеке `lua-resty-*`.

`Metatable` ведет себя как перегрузка операторов; например, мы можем перегрузить `__add` для вычисления конкатенации двух Lua-массивов или `__tostring` для определения функций преобразования в строки.

Lua, с другой стороны, предоставляет две функции для работы с метатаблицами.

- Первая — это `setmetatable(table, metatable)`, которая устанавливает метатаблицу для таблицы.
- Вторая — это `getmetatable(table)`, которая получает метатаблицу таблицы.

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

```
$ resty -e ' local version = {
  major = 1,
  minor = 1,
  patch = 1
  }
version = setmetatable(version, {
    __tostring = function(t)
      return string.format("%d.%d.%d", t.major, t.minor, t.patch)
    end
  })
  print(tostring(version))
'
```

Сначала мы определяем таблицу с именем `version`, и, как видите, цель этого кода — вывести номер версии из `version`. Однако мы не можем просто напечатать `version`. Попробуйте сделать это, и вы увидите, что печать напрямую выведет только адрес таблицы.

```
print(tostring(version))
```

Поэтому нам нужно настроить функцию преобразования строк для этой таблицы, которая и есть `__tostring`, и здесь на помощь приходит метатаблица. Мы используем `setmetatable`, чтобы переопределить метод `__tostring` таблицы `version`, чтобы вывести номер версии: 1.1.1.

Помимо `__tostring`, мы часто переопределяем следующие два метаметода в метатаблице в реальных проектах.

**Один из них — \_\_index**. Когда мы ищем элемент в таблице, мы сначала ищем его непосредственно в таблице, и если не находим, то переходим к `__index` метатаблицы.

В следующем примере мы удаляем `patch` из таблицы `version`.

```
$ resty -e ' local version = {
  major = 1,
  minor = 1
  }
version = setmetatable(version, {
     __index = function(t, key)
         if key == "patch" then
             return 2
         end
     end,
     __tostring = function(t)
      return string.format("%d.%d.%d", t.major, t.minor, t.patch)
    end
  })
  print(tostring(version))
'
```

В этом случае `t.patch` не получает значение, поэтому он переходит к функции `__index`, которая выводит 1.1.2.

`__index` может быть не только функцией, но и таблицей, и если вы попробуете выполнить следующий код, вы увидите, что они достигают того же результата.

```
$ resty -e ' local version = {
  major = 1,
  minor = 1
  }
version = setmetatable(version, {
     __index = {patch = 2},
     __tostring = function(t)
      return string.format("%d.%d.%d", t.major, t.minor, t.patch)
    end
  })
  print(tostring(version))
'
```

**Другой метаметод — \_\_call**. Он похож на функтор, который позволяет вызывать таблицу.

Давайте продолжим на основе кода выше, который выводит номер версии, и посмотрим, как вызвать таблицу.

```
$ resty -e '
local version = {
  major = 1,
  minor = 1,
  patch = 1
  }
local function print_version(t)
     print(string.format("%d.%d.%d", t.major, t.minor, t.patch))
end
version = setmetatable(version,
     {__call = print_version})
  version()
'
```

В этом коде мы используем `setmetatable`, чтобы добавить метатаблицу к таблице `version`, и метаметод `__call` внутри нее указывает на функцию `print_version`. Поэтому, если мы попытаемся вызвать `version` как функцию, здесь будет выполнена функция `print_version`.

И `getmetatable` — это операция, парная с `setmetatable`, чтобы получить установленную метатаблицу, как в следующем коде.

```
$ resty -e ' local version = {
  major = 1,
  minor = 1
  }
version = setmetatable(version, {
     __index = {patch = 2},
     __tostring = function(t)
      return string.format("%d.%d.%d", t.major, t.minor, t.patch)
    end
  })
  print(getmetatable(version).__index.patch)
'
```

Помимо этих трех метаметодов, о которых мы говорили сегодня, есть некоторые редко используемые метаметоды, которые вы можете изучить в [документации](http://lua-users.org/wiki/MetamethodsTutorial), когда столкнетесь с ними.

## Объектно-ориентированное программирование

Наконец, давайте поговорим об объектно-ориентированном программировании. Как вы, возможно, знаете, Lua не является объектно-ориентированным языком, но мы можем использовать метатаблицы для реализации ОО.

Давайте рассмотрим практический пример. [lua-resty-mysql](https://github.com/openresty/lua-resty-mysql/blob/master/lib/resty/mysql.lua) — это официальный клиент MySQL для OpenResty, и он использует метатаблицы для **симуляции** классов и методов классов, которые используются следующим образом.

```
    $ resty -e 'local mysql = require "resty.mysql" -- сначала ссылаемся на библиотеку lua-resty
    local db, err = mysql:new() -- Создаем новый экземпляр класса
    db:set_timeout(1000) -- Вызов методов класса
```

Вы можете выполнить этот код напрямую с помощью команды `resty`. Эти строки кода легко понять; единственное, что может вызвать у вас затруднения, это:

**Почему при вызове метода класса используется двоеточие вместо точки?**

На самом деле, и двоеточие, и точка здесь допустимы, и `db:set_timeout(1000)` и `db.set_timeout(db, 1000)` абсолютно эквивалентны. Двоеточие — это синтаксический сахар в Lua, который позволяет опускать первый аргумент `self` функции.

Как мы все знаем, перед исходным кодом нет секретов, поэтому давайте посмотрим на конкретную реализацию, соответствующую этим строкам кода, чтобы вы могли лучше понять, как моделировать объектно-ориентированное программирование с помощью метатаблиц.

```
local _M = { _VERSION = '0.21' } -- Используем таблицу для симуляции класса
local mt = { __index = _M } -- mt — это сокращение от metatable, __index ссылается на сам класс
-- Конструктор класса
function _M.new(self)
    local sock, err = tcp()
    if not sock then
          return nil, err
    end
    return setmetatable({ sock = sock }, mt) -- пример симуляции классов с использованием таблицы и метатаблицы
end

-- Методы класса
function _M.set_timeout(self, timeout) -- Используем аргумент self для получения экземпляра класса, с которым хотим работать
  local sock = self.sock
  if not sock then
      return nil, "not initialized"
  end

  return sock:settimeout(timeout)
end
```

Таблица `_M` симулирует класс, инициализированный одной переменной-членом `_VERSION`, и впоследствии определяет методы-члены, такие как `_M.set_timeout`. В конструкторе `_M.new(self)` мы возвращаем таблицу, метатаблица которой — `mt`, а метаметод `__index` `mt` указывает на `_M`, так что возвращаемая таблица симулирует экземпляр класса `_M`.

## Итог

Ну что ж, это завершает основное содержание на сегодня. Таблицы и метатаблицы широко используются в библиотеках `lua-resty-*` OpenResty и проектах на основе OpenResty. Я надеюсь, что этот урок облегчит вам чтение и понимание исходного кода.

В Lua есть и другие стандартные функции, которые мы изучим вместе в следующем уроке.

Наконец, я хотел бы оставить вам вопрос для размышления. Почему библиотека `lua-resty-mysql` моделирует ОО как слой обертки? Добро пожаловать обсудить этот вопрос в разделе комментариев, и приглашаю вас поделиться этой статьей с вашими коллегами и друзьями, чтобы мы могли общаться и прогрессировать вместе.

Share article link

Share

Related Posts

[![Что делает OpenResty таким особенным](https://static.api7.ai/2022/10/14/634974e090ab4.png)](https://api7.ai/learning-center/openresty/openresty-getting-started)

## [Что делает OpenResty таким особенным](https://api7.ai/learning-center/openresty/openresty-getting-started)

[![Что делать, если возникает конфликт между правилами Lua и конфигурацией NGINX?](https://static.api7.ai/2022/10/14/634974e117ebd.png)](https://api7.ai/learning-center/openresty/conflict-between-lua-rules-and-nginx)

## [Что делать, если возникает конфликт между правилами Lua и конфигурацией NGINX?](https://api7.ai/learning-center/openresty/conflict-between-lua-rules-and-nginx)

Table of Contents

- [Функции библиотеки таблиц](#%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8-%D0%B1%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B8-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86)

  - [`table.getn` Получение количества элементов](#tablegetn-%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BA%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%B0-%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2)
  - [`table.remove` Удаление указанного элемента](#tableremove-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE-%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0)
  - [`table.concat` Функция объединения элементов](#tableconcat-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F-%D0%BE%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F-%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2)
  - [`table.insert` Вставка элемента](#tableinsert-%D0%B2%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0-%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0)

- [Расширенные функции таблиц LuaJIT](#%D1%80%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86-luajit)

  - [`table.new(narray, nhash)` Создание новой таблицы](#tablenewnarray-nhash-%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BD%D0%BE%D0%B2%D0%BE%D0%B9-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B)
  - [`table.clear()` Очистка таблицы](#tableclear-%D0%BE%D1%87%D0%B8%D1%81%D1%82%D0%BA%D0%B0-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B)

- [Расширенные функции таблиц OpenResty](#%D1%80%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86-openresty)

- [Метатаблицы](#%D0%BC%D0%B5%D1%82%D0%B0%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B)

- [Объектно-ориентированное программирование](#%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

- [Итог](#%D0%B8%D1%82%D0%BE%D0%B3)
