Перейти к основному содержимому

Рекомендации по разработке на Lua

Разработчику Архитектору

Рекомендации по разработке на Lua​

Введение в культуру кода на Lua​

Качественный код на Lua сочетает лаконичность языка с предсказуемостью поведения. Язык предоставляет минималистичный набор конструкций, что делает ответственность за читаемость и структуру полностью лежащей на разработчике. Основные опорные точки культуры кода на Lua — последовательность в именовании, явное управление состоянием таблиц, бережное отношение к производительности в циклах и чёткое разделение ответственностей между модулями.

Сообщество Lua выработало устойчивые соглашения, отражённые в стилевых руководствах от разработчиков ядра языка, игровых компаний и инфраструктурных проектов. Эти соглашения формируют основу профессиональной разработки на языке.


Именование сущностей​

Общие принципы именования​

Имена переменных, функций и модулей пишутся в стиле snake_case. Этот стиль устоялся в сообществе как стандарт де-факто и используется в официальной документации, стандартной библиотеке и большинстве популярных фреймворков.

Примеры корректных имён:

local player_health = 100
local max_inventory_slots = 20
local calculate_damage = function(attacker, target)
-- реализация
end

Константы объявляются заглавными буквами с подчёркиваниями:

local MAX_PLAYERS = 64
local DEFAULT_TIMEOUT = 30
local PHYSICS_GRAVITY = 9.81

Имена модулей соответствуют пути к файлу в файловой системе с заменой разделителей пути на точки:

-- Файл: game/entities/player.lua
local Player = require("game.entities.player")

Специфика именования таблиц и объектов​

Таблицы, представляющие объекты с поведением, именуются с заглавной буквы в стиле PascalCase:

local Vector2 = {}
local GameObject = {}
local NetworkManager = {}

Таблицы, используемые как простые контейнеры данных или конфигурации, именуются в snake_case:

local player_stats = { health = 100, mana = 50 }
local ui_config = { font_size = 14, padding = 8 }

Имена итераторов в циклах отражают содержимое коллекции:

for index, item in ipairs(inventory_items) do
-- обработка предмета
end

for entity_id, entity in pairs(active_entities) do
-- обработка сущности
end

Избегайте однобуквенных имён за исключением стандартных математических обозначений или коротких циклов:

-- допустимо в узком контексте
for i = 1, 10 do
sum = sum + i * i
end

-- недопустимо для бизнес-логики
local x = get_user_data()
process(x) -- непонятно, что представляет x

Оформление кода​

Отступы и пробелы​

Используйте два пробела для отступов. Табуляция не применяется. Открывающая скобка блока размещается на той же строке, что и управляющая конструкция. Закрывающая скобка выравнивается по отступу управляющей конструкции.

if player.health <= 0 then
player.is_alive = false
trigger_death_animation(player)
end

for i = 1, #enemies do
if enemies[i]:is_visible() then
enemies[i]:update_position(delta_time)
end
end

Добавляйте пробелы вокруг бинарных операторов и после запятых:

local damage = base_damage * (1 + crit_multiplier)
local position = { x = 10.5, y = 20.3, z = 0.0 }

Не добавляйте пробелы внутри скобок вызова функции или индексации таблицы:

-- правильно
local result = calculate(10, 20)
local value = config["timeout"]

-- неправильно
local result = calculate( 10, 20 )
local value = config[ "timeout" ]

Длинные строки и переносы​

Переносите длинные строки вызовов функций или выражений на новую строку с дополнительным отступом в два пробела. Запятые размещайте в конце строки:

local success, result = network.request(
"POST",
"/api/v1/users",
{ username = "player1", password = "secret" },
{ timeout = 5000, headers = { ["Content-Type"] = "application/json" } }
)

Для таблиц с множеством элементов используйте вертикальное оформление:

Код ITЗагрузка примера кода…


Условные конструкции и циклы​

Всегда используйте блоки then/end даже для однострочных условий. Это предотвращает ошибки при расширении логики и улучшает читаемость:

-- правильно
if is_debug_mode then
log_debug_message("Entering combat zone")
end

-- неправильно
if is_debug_mode then log_debug_message("Entering combat zone") end

Для цепочек условий применяйте последовательное выравнивание:

if player.stamina > 50 then
perform_sprint()
elseif player.stamina > 20 then
perform_jog()
else
perform_walk()
end

Структура проекта и организация файлов​

Иерархия каталогов​

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

project/
├── main.lua -- точка входа приложения
├── config/ -- конфигурационные файлы
│ ├── default.lua
│ └── production.lua
├── core/ -- ядро движка или фреймворка
│ ├── event.lua
│ ├── timer.lua
│ └── math.lua
├── entities/ -- игровые сущности или бизнес-объекты
│ ├── player.lua
│ ├── enemy.lua
│ └── projectile.lua
├── systems/ -- системы обработки (ECS-стиль)
│ ├── physics.lua
│ ├── rendering.lua
│ └── audio.lua
├── ui/ -- пользовательский интерфейс
│ ├── hud.lua
│ ├── menu.lua
│ └── inventory.lua
├── utils/ -- утилитарные функции
│ ├── string.lua
│ ├── table.lua
│ └── validation.lua
└── tests/ -- тесты
├── unit/
└── integration/

Каждый файл представляет собой самостоятельный модуль, возвращающий таблицу с публичным интерфейсом через return.


Модульная система​

Модули объявляются через локальную таблицу, которая возвращается в конце файла:

Код ITЗагрузка примера кода…

Импорт модулей выполняется через require с указанием пути относительно корня проекта:

local String = require("utils.string")
local Player = require("entities.player")

local cleaned = String.trim(" hello world ")
local words = String.split(cleaned, " ")

Избегайте глобальных переменных. Все сущности должны быть либо локальными, либо явно экспортированы через модульную систему. Глобальное пространство имён зарезервировано для стандартной библиотеки Lua и критически важных системных компонентов.


Работа с таблицами​

Инициализация и манипуляции​

Пустые таблицы объявляются через {}. Для таблиц, используемых как массивы, применяйте функции table.insert, table.remove и # для получения длины:

local items = {}
table.insert(items, "Sword")
table.insert(items, "Shield")
table.insert(items, "Potion")

for i = 1, #items do
print(items[i])
end

Для таблиц-словарей используйте прямую индексацию строковыми ключами:

local settings = {
volume = 0.75,
fullscreen = true,
language = "en"
}

settings.resolution = "1920x1080"

Избегайте смешивания числовых и строковых ключей в одной таблице без явной необходимости. Такая практика затрудняет понимание семантики таблицы и приводит к ошибкам при итерации.


Копирование таблиц​

Глубокое копирование таблиц требует рекурсивной реализации. Для поверхностного копирования используйте итерацию:

Код ITЗагрузка примера кода…

В производительных критических участках избегайте частого создания таблиц внутри циклов. Предварительно аллоцируйте рабочие таблицы или используйте пулы объектов.


Функции и замыкания​

Объявление и сигнатуры​

Функции объявляются через ключевое слово function или как значения таблицы:

Код ITЗагрузка примера кода…

Синтаксис function table:name() автоматически добавляет параметр self в начало списка аргументов. Это эквивалентно function table.name(self, ...).


Параметры и возврат значений​

Функции в Lua могут принимать переменное число аргументов через ... и возвращать несколько значений:

local function min_max(...)
local args = { ... }
local min, max = args[1], args[1]
for i = 2, #args do
if args[i] < min then min = args[i] end
if args[i] > max then max = args[i] end
end
return min, max
end

local lowest, highest = min_max(3, 7, 2, 9, 1)
-- lowest = 1, highest = 9

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

Код ITЗагрузка примера кода…

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


Замыкания и состояние​

Замыкания позволяют сохранять локальное состояние между вызовами функции:

local function create_counter(initial)
local count = initial or 0
return function()
count = count + 1
return count
end
end

local counter = create_counter(10)
print(counter()) -- 11
print(counter()) -- 12

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


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

Прототипное наследование​

Lua реализует ООП через таблицы и метатаблицы. Базовый шаблон класса:

Код ITЗагрузка примера кода…


Наследование через метатаблицы​

Наследование реализуется через установку __index дочернего класса на родительский:

Код ITЗагрузка примера кода…

Конструктор дочернего класса вызывает конструктор родителя для инициализации базовых полей. Переопределённые методы вызывают родительские реализации через Animal.speak(self) при необходимости.


Приватные поля через замыкания​

Для эмуляции приватных полей используйте замыкания:

Код ITЗагрузка примера кода…

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


Обработка ошибок​

Защищённые вызовы​

Используйте pcall для перехвата ошибок выполнения:

local success, result = pcall(function()
return risky_operation()
end)

if success then
process(result)
else
log_error("Operation failed: " .. result)
end

Для получения стека вызовов при ошибке применяйте xpcall с обработчиком ошибок:

local function error_handler(err)
return debug.traceback(err, 2)
end

local success, result = xpcall(risky_operation, error_handler)
if not success then
log_error(result) -- содержит стек вызовов
end

Утверждения и валидация​

Функция assert используется для проверки предусловий и инвариантов:

function divide(a, b)
assert(type(a) == "number", "First argument must be a number")
assert(type(b) == "number", "Second argument must be a number")
assert(b ~= 0, "Division by zero")
return a / b
end

В производственном коде избегайте чрезмерного использования assert для проверки внешних данных. Вместо этого применяйте явную валидацию с возвратом ошибок:

Код ITЗагрузка примера кода…

Такой подход позволяет гибко обрабатывать ошибки на разных уровнях приложения без прерывания выполнения.


Комментарии и документация​

Виды комментариев​

Однострочные комментарии начинаются с --. Многострочные комментарии оформляются через --[[ ... ]]:

-- Расчёт урона с учётом брони цели
local damage = calculate_damage(attacker, target)

--[[
Система квестов обрабатывает:
- активацию триггеров
- отслеживание прогресса
- начисление наград
]]
quest_system:update(delta_time)

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


Документирование модулей​

Модули сопровождаются заголовочным комментарием с описанием назначения и примерами использования:

Код ITЗагрузка примера кода…

Для публичных функций указывайте типы параметров и возвращаемых значений в комментариях:

--[[
Создаёт новую частицу с указанными параметрами.

@param position table {x: number, y: number} Начальная позиция
@param velocity table {x: number, y: number} Начальная скорость
@param lifetime number Время жизни в секундах
@param color table {r: number, g: number, b: number, a: number} Цвет
@return table Частица с методами обновления и отрисовки
]]
function ParticleSystem:create_particle(position, velocity, lifetime, color)
-- реализация
end

Такой подход упрощает навигацию по кодовой базе и интеграцию с инструментами автодополнения.


Производительность и оптимизация​

Аллокации памяти​

Частое создание таблиц и строк внутри циклов приводит к нагрузке на сборщик мусора. Предварительно аллоцируйте рабочие структуры:

-- неэффективно
for i = 1, 1000 do
local temp = { x = i * 0.1, y = math.sin(i) }
process(temp)
end

-- эффективно
local temp = {}
for i = 1, 1000 do
temp.x = i * 0.1
temp.y = math.sin(i)
process(temp)
end

Для временных объектов используйте пулы:

Код ITЗагрузка примера кода…


Локальные переменные​

Локальные переменные быстрее глобальных и полей таблиц. Кэшируйте часто используемые функции и значения:

-- медленно
for i = 1, 1000000 do
table.insert(results, math.sin(i) * math.cos(i))
end

-- быстро
local sin, cos, insert = math.sin, math.cos, table.insert
for i = 1, 1000000 do
insert(results, sin(i) * cos(i))
end

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


Строковые операции​

Конкатенация строк через .. создаёт новые объекты. Для сборки длинных строк используйте таблицу с последующим table.concat:

-- неэффективно
local result = ""
for i = 1, 1000 do
result = result .. items[i] .. "\n"
end

-- эффективно
local parts = {}
for i = 1, 1000 do
table.insert(parts, items[i])
table.insert(parts, "\n")
end
local result = table.concat(parts)

Этот подход снижает количество аллокаций и ускоряет обработку текста в циклах.


Тестирование кода на Lua​

Структура тестов​

Тесты организуются параллельно основной кодовой базе с сохранением иерархии:

project/
├── src/
│ └── utils/
│ └── string.lua
└── tests/
└── utils/
└── string_spec.lua

Каждый тестовый файл содержит набор проверок для соответствующего модуля:

Код ITЗагрузка примера кода…


Тестирование асинхронного кода​

Для проверки асинхронных операций используйте таймауты и колбэки:

Код ITЗагрузка примера кода…

В производственных проектах применяйте специализированные тестовые фреймворки с поддержкой асинхронных проверок, такие как busted с расширениями для кооперативной многозадачности.


Инструменты разработки​

Статический анализ​

Luacheck анализирует код на предмет неиспользуемых переменных, глобальных утечек и стилевых нарушений. Конфигурация размещается в .luacheckrc:

std = "lua51"
globals = { "love", "ngx" } -- разрешённые глобальные для конкретных сред
max_line_length = 100
unused_args = false

Запуск анализа:

luacheck src/ --globals love,ngx

Форматирование кода​

LuaFormatter автоматизирует применение стилевых правил. Конфигурация в .lua-format:

indent_width = 2
use_tab = false
column_limit = 100
continuation_indent_width = 2

Форматирование проекта:

lua-format -i src/**/*.lua

Отладка​

Встроенный отладчик через debug модуль позволяет устанавливать точки останова и инспектировать стек вызовов:

debug.sethook(function(event, line)
if event == "line" and line == 42 then
print("Reached line 42")
print(debug.traceback())
end
end, "l")

Для сложных проектов применяйте интегрированные отладчики в средах разработки — ZeroBrane Studio, Visual Studio Code с расширением Lua Debug или среды, предоставляемые игровыми движками (LÖVE2D, Defold, Roblox Studio).