Итерирование таблиц в Lua: циклы, ipairs и pairs
Опубликовано:
Поскольку в Lua есть только один составной тип — таблица, решение, как ее перебирать, должен принимать сам программист. В итерирующем for (цикле перебора) обычно вызывают ipairs для обхода таблиц, которые хранятся как массивы с последовательной индексацией от 1. Во всех остальных случаях используют pairs. Но так ли все просто на практике?
В данной статье дается краткий обзор всех циклов в Lua, а затем разбираются нюансы итерации, безопасное удаление элементов и создание кастомного генератора.
В Lua имеется полный арсенал циклов: классический while, с постусловием repeat...until, for числовой и для итерирования.
local sum = 0 while true do local input = io.read('n') -- считываем число if input == nil then break -- прерываем цикл, если введено не число end sum = sum + input end print(sum)
local sum = 0 repeat local i = io.read('n') sum = sum + (i or 0) -- если i равен nil, подставится 0 until i == nil print(sum)
4 -3 0.5 stop 1.5
local lines = {} for i = 1, 3 do lines[i] = io.read() end for i, v in ipairs(lines) do print(i .. ': ' .. v) end
black grey yellow green blue 1: black grey 2: yellow green 3: blue
Однако в Lua синтаксис цикла for...in..., который для таблиц используется чаще, ожидает после in наличие функции-итератора. На каждой итерации Lua неявно вызывает эту функцию. Если она возвращает любое значение, кроме nil, цикл продолжается.
В случае переобхода таблиц используются итераторные генераторы ipairs и pairs. Они создают (возвращают) функции-итераторы, которые и использует цикл for...in....
Возврат именно пары из ключа и значения — это специфика создаваемых функциями ipairs и pairs итераторов, которая продиктована особенностями работы с таблицами. На самом деле в Lua количество переменных перед in может быть любым. Если их больше необходимого, лишним присваивается nil. Если меньше, данные, которым не досталось переменной, теряются. Так код ниже выведет только индексы, а не значения элементов или парные кортежи, как это было бы в Python:
local arr = {'one', 'two', 'three'} for i in ipairs(arr) do io.write(i .. ' ') end print()
1 2 3
Какое бы количество переменных вы ни написали, Lua всегда принимает решение о продолжении или завершении цикла только по первой. Если она после очередного вызова итератора получает nil, цикл останавливается. Любое другое значение (даже false) заставляет цикл выполнять свое тело.
Если в Python списки и словари — это разные типы данных, и цикл for работает с ними по-разному, то в Lua есть только один составной тип — таблица. И хотя в памяти она может храниться и как массив и как хеш-таблица, решение о способе перебора отдается в руки программисту. Когда вы вызываете ipairs или pairs, вы явно принуждаете интерпретатор использовать один из двух конкретных алгоритмов обхода: перебирать таблицу как упорядоченную последовательность или как неупорядоченный словарь.
Функция ipairs используется для таблиц-последовательностей (массивов, списков) и гарантирует упорядоченное извлечение элементов. Однако циклический обход таблицы останавливается не только после "извлечения" всех ее элементов, также это произойдет при встрече первого nil, то есть "дыры в массиве". Также если у элемента ключом является не целое число, то он просто пропускается.
local arr = {10, t = 50, 20, [5] = 30, 40} for i, v in ipairs(arr) do print(i, v) end
1 10 2 20 3 40
Здесь число 30 с индексом 5 будет выводиться, если в таблицу добавить еще одно значение без явного ключа или с индексом 4. А в предложенном выше варианте четвертого элемента просто нет, то есть этот элемент имеет значение nil.
Функция pairs обходит все элементы, независимо от типа данных их ключей. В основном используется для таблиц-словарей (записей, объектов, классов). Порядок обхода хэш-ключей может несоответствовать порядку следования элементов в таблице.
local d = {name='Tom', level=4} for i, v in pairs(d) do print(i, v) end
level 4 name Tom
В случае последовательностей также можно использовать pairs, это имеет смысл, если есть подозрение на "дыру" в массиве. Однако только ipairs гарантирует упорядоченность, а также работает быстрее, так как здесь не высчитываются хэши и не ищутся ключи внутри структуры таблицы. Итератор просто выполняет инкремент индекса +1 на каждом шаге.
Важно помнить, что когда мы ключу присваиваем nil, элемент просто исчезает из таблицы:
local data = {a=10, b=20, 30, c=40} data.a = nil data[1] = nil for i, v in pairs(data) do print(i, v) end
c 40 b 20
В Lua именно так удаляют элементы из хеш-таблицы. Но так не следует делать для массивов, чтобы не образовывались nil-дыры. Если последовательность должна таковой и оставаться, надо использовать функцию table.remove. Пример ниже удаляет элементы по условию, сохраняя хранение таблицы в памяти как массива:
local t = {10, 4, 12, 7, 20} -- #t длина массива, -1 шаг назад for i = #t, 1, -1 do if t[i] < 10 then table.remove(t, i) end end for i, v in ipairs(t) do print(i, v) end
1 10 2 12 3 20
Итераторные генераторы, или фабрики итераторов, — это обычные функции в Lua. Единственное, что делает функцию "генератором" — это её архитектурное назначение: она создана для того, чтобы конструировать и возвращать другие функции (итераторы). Рассмотрим пример кастомного итераторного генератора, который работает так же, как ipairs, но перебирает массив с конца:
local function r_ipairs(t) local index = #t + 1 -- стартовая позиция (за границей массива) -- Генератор возвращает итератор (анонимную функцию) return function() index = index - 1 if index > 0 then return index, t[index] -- возвращаем пару else return nil -- стоп-сигнал для цикла for end end end local array = {'apple', 'banana', 'pineapple'} for i, v in r_ipairs(array) do print(i, v) end
3 pineapple 2 banana 1 apple
Интерпретатор Lua в заголовке цикла for вызывает r_ipairs(array) только один раз. Функция r_ipairs при выполнении своего тела создает внутри себя переменную index и возвращает созданную анонимную функцию. Цикл for подхватывает эту анонимную функцию и начинает вызывать уже её на каждом шаге, пока она не вернет nil.