Множественный возврат из функций и особенности передачи аргументов в Lua
Опубликовано:
Описываем, как в Lua, когда функция возвращает несколько значений, происходит присваивание переменным. Знакомимся с выражением vararg (...), с помощью которого функции могут принимать и передавать любое количество аргументов. Узнаем, что несмотря на отсутствие в Lua механизма именованных параметров, имеется возможность передавать аргументы по ключам с помощью таблиц.
Множественный возврат и его особенности в Lua
В Lua есть настоящий множественный возврат значений из функций. В примере ниже мы доказываем это с помощью функций select и table.pack. Это, например, отличается от Python, в котором при перечислении нескольких значений после return, происходит неявная упаковка данных в один кортеж.
local function get_coordinates() local x = math.random(-50, 100) local y = math.random(-50, 100) local z = math.random(25) return x, y, z end print(get_coordinates()) print(select('#', get_coordinates())) local point1 = table.pack(get_coordinates()) print(point1[1], point1[2], point1[3], point1.n)
78 91 14 3 83 -49 14 3
Если функция select() в качестве первого аргумента получает '#', то возвращает общее количество переданных в неё элементов. Функция table.pack() собирает все значения в массив и автоматически добавляет в него поле n, в котором хранится их количество.
В Lua можно сразу присвоить полученные из функции значения отдельным переменным:
local a, b, c = get_coordinates() print(a, b, c)
-12 68 10
Однако когда количество переменных не соответствует количеству возвращенных значений или когда в присваивании результата участвуют "посторонние" значения, поведение Lua нетривиально:
local a = get_coordinates() print('Только одно число из функции:', a) local k, l, m = get_coordinates(), 999 print('Только одно число из функции:', k, l, m) local x, y, z = 999, get_coordinates() print('Только два числа из функции:', x, y, z) -- Упаковка в таблицу local t1 = { get_coordinates() } --> Сохранятся все значения local t2 = { get_coordinates(), 999 } --> Только первое и 999 print(table.concat(t1, ', ')) print(table.concat(t2, ', '))
Только одно число из функции: -22 Только одно число из функции: 17 999 nil Только два числа из функции: 999 -49 69 54, -43, 9 34, 999
Код выше показывает, что если переменных меньше, чем возвращенных значений, лишние данные просто не сохраняются (происходит динамическое усечение). Если переменных больше, то оставшиеся получают nil (динамическое дополнение).
Если функция вызвана последней в списке присваивания или аргументов, раскрываются все её значения. Если она стоит в начале или посередине, сохраняется только первое значение, а остальные отбрасываются.
Поэтому в Lua необходимо быть внимательными при вызовы функций внутри таблиц, если эти функции возвращают несколько значений. Так {foo()} упакует значения, но {foo(), bar()} приведет к потере части данных из первой функции.
Функции с переменным числом аргументов (vararg)
Три точки ... в Lua называются вар-арг (vararg) или переменным числом аргументов (variable arguments). Этот инструмент позволяет функциям принимать произвольное количество аргументов. Непосредственно при вызове функции и передаче аргументов Lua упаковывает их в скрытый упорядоченный список.
Часто пользовательская функция является "посредником" и передает vararg в другую функцию:
local function log_with_prefix(prefix, ...) -- prefix заберет первый аргумент, а всё остальное уйдет в print print('[' .. prefix .. ']', ...) end log_with_prefix('INFO', 10, 'готово', true) -- Выведет: [INFO] 10 готово true
Если необходимо перебрать все переданные элементы, проще поместить их в таблицу. В Lua 5.2+ для этого обычно используют table.pack(...), так как это безопасный способ, сохраняющий точную длину в поле n. Ранее часто использовали {...}, но в таком варианте теряются nil-элементы.
local function sum(...) local args = table.pack(...) local total = 0 for i = 1, args.n do total = total + (args[i] or 0) end return total end print(sum(5, 10, 15)) -- Выведет: 30
Чтобы не нагружать сборщик мусора созданием лишней таблицы в памяти, можно извлекать элементы из ... с помощью уже ранее упомянутой функции select.
local function process_coordinates(point, ...) print('Точка ' .. point) local count = select('#', ...) -- Узнали количество -- Сами данные лежат в ..., их можно перебрать: for i = 1, count do local coord = select(i, ...) print('Координата ' .. i .. ':', coord) end end process_coordinates('A', 10, 20) process_coordinates('B', -50, 100, 20)
Точка A Координата 1: 10 Координата 2: 20 Точка B Координата 1: -50 Координата 2: 100 Координата 3: 20
На самом деле когда функция select первым аргументом получает число n, она отсекает первые n - 1 аргументы и возвращает n-й аргумент, а также все остальные, которые идут за ним.
local a, b, c = select(2, 'яблоко', 'банан', 'груша', 'слива') print(a) -- 'банан' print(b) -- 'груша' print(c) -- 'слива'
в Lua весь файл-скрипт при запуске неявно компилируется в одну функцию, а в ... на уровне файла записываются аргументы командной строки, с которыми этот скрипт был запущен:
-- Если запустить файл из консоли: lua script.lua привет 123 local arg1, arg2 = ... print(arg1) -- привет print(arg2) -- 123
Именованные аргументы в Lua через таблицы-словари
Паттерн Named Arguments (именованные аргументы) снимает необходимость помнить строгий порядок параметров функции, а также позволяет делать их необязательными. В Lua нет такого встроенного механизма, как в том же Python. Вместо для реализации этой задачи здесь используются таблицы-словари (ассоциативные массивы с именованными ключами). Например, func({name="Иван", age=25}).
Также для облегчения восприятия в Lua имеется синтаксический сахар: если функция принимает один аргумент-таблицу или строку, круглые скобки можно опустить. Например: render { width = 1920, height = 1080, format = "png" }.
Пример функции, принимающей конфигурацию персонажа со значениями по умолчанию и возвращающей словарь с его итоговым состоянием:
-- Функция принимает один аргумент — таблицу-словарь local function create_character(options) -- Задаем значения по умолчанию, если какие-то ключи не переданы local name = options.name or 'Безымянный' local role = options.role or 'Воин' local hp = options.hp or 100 local mp = options.mp or 50 -- Логика: создаем персонажа... local final_power = hp + (mp * 1.5) -- Возвращаем результат тоже в виде таблицы-словаря return { uid = math.random(1000, 9999), display_name = name .. ' (' .. role .. ')', is_alive = true, power_score = final_power } end local hero = create_character { name = 'Арагорн', role = 'Следопыт', hp = 120 -- mp мы не передали, сработает значение по умолчанию (50) } print(hero.display_name) print(hero.power_score) print(hero.uid)
Арагорн (Следопыт) 195.0 2737