Таблицы в Lua как основа для всех сложных типов данных

Опубликовано:

Разработчиков на других языках при знакомстве с Lua может удивить, что здесь есть только один структурированный тип table, от которого создаются таблицы. Привычных списков или массивов, словарей или отображений, множеств и другого в Lua нет. На первый взгляд это кажется ограничением, но на практике таблицы в Lua обладают такой гибкостью, которая позволяет эмулировать все остальное под разные задачи.

В Lua таблицы являются единственным составным типом данных. Однако их гибкость позволяет рассматривать таблицы скорее как основу для всех остальных сложных типов.

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

local t = {}  -- создается пустая таблица

t.a = 10  -- первое поле
t.b = 'hello'  -- второе поле

print(t.a, t.b)  -- 10	hello

Может показаться, что это поля (идентификаторы), которые находятся внутри объекта-таблицы. На самом деле выражение типа t.a — это синтаксический сахар языка Lua. В таблице не будет переменной a, там окажется ключ-строка "a". Убедиться в этом можно так:

print(t['a'], t['b'])  -- 10	hello

t['c'] = 2.3
print(t.c)  -- 2.3

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

Такие таблицы в Lua подобны словарям в Python. Из этого следует, что ключи в них не обязаны быть строками. В Lua ключом может быть любой тип данных, кроме nil (этот тип имеет только одно одноименное значение):

local t = {['qty']=100, b=5, [1]='hello', [0.1]='дробь'}

print(t.qty)  -- 100
print(t.b)  -- 5
print(t[1])  -- hello
print(t[0.1])  -- дробь

Синтаксис полей-идентификаторов допустим и при заполнении таблицы в момент ее создания. Поэтому таблицы в Lua могут выглядеть и использоваться как записи:

local book1 = {title="Hello World!",
               pages=10,
               author=nil}

Учитывая, что полям можно присваивать функции в качестве значений, таблицы в Lua также являются основой для объектно-ориентированного программирования (класс хранит методы, а объекты — это записи), что будет рассмотрено в отдельной статье.

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

local n = {'one', 'two', 'three', 'four'}
print(n[1]) -- one
print(#n)   -- 4 (длина последовательности)

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

Чтобы при удалении элементов последовательности гарантированно сохранить ее хранение в памяти в виде массива, следует использовать функцию remove из модуля table. При обычном присваивании элементу значения nil его ячейка очищается, но в массиве образуется "дыра". Это нарушает целостность последовательности и в дальнейшем, при изменении размеров таблицы, может привести к тому, что оставшиеся элементы переместятся в хеш-таблицу.

print(n[2])  -- two
table.remove(n, 2)
print(n[2])  -- three
print(#n)    -- 3

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

local t = {['qty']=100, b=5, [1]='hello', [0.1]='дробь'}
t.b = nil
t[1] = nil

-- подсчет количества элементов, 
-- встроенной функции для хеш-таблиц нет
local count = 0
for _ in pairs(t) do
    count = count + 1
end
print(count)  -- 2

Lua позволяет создавать смешанные таблицы, в которых часть данных хранится в массиве, а другая в хеш-таблице:

local n = {'one', 'two', a=100, 'four', b=20}
print(n[1]) -- one
print(n[2]) -- two
print(n[3]) -- four
print(n.a)  -- 100
print(n.b)  -- 20

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

local fruits = {
    apple = true,
    banana = true,
    orange = true
}

print(fruits.apple)      -- true
print(fruits.pineapple)  -- nil

fruits.pear = true    -- добавление
fruits.banana = true  -- ничего не изменится
fruits.apple = nil    -- удаление элемента из множества

В конце отметим, что таблицы в Lua широко используются не только для создания списков, словарей и объектов, также в качестве модулей (описано в конце урока про переменные) и для хранения глобальной области видимости _G (глобальные переменные не приветствуются, но когда они появляются, то добавляются сюда). Таблица _G создаётся автоматически в момент запуска интерпретатора Lua. По умолчанию она уже имеет содержимое: хранит в себе ссылки на все встроенные функции, стандартные библиотеки (модули), системные переменные.

Кроме этого, в Lua на базе таблиц создают многомерные структуры (матрицы, графы), разреженные массивы и эффективные очереди, реализуют механизмы кэширования данных (мемоизации), а также настраивают прокси-объекты для контроля доступа к памяти и реактивного программирования