Типы данных и объявление переменных в Lua
Дальше: Работа со строками, таблицами и файлами · Справочник Lua · Работа с типами (ниже)
Типы данных и объявление переменных в Lua
Типизация
Система типов в Lua построена на принципах динамической типизации и минимализма.
Динамическая типизация подразумевает, что тип значения ассоциируется не с переменной, а со значением. Технически, переменная представляет собой ссылку (или имя) на объект определённого типа, при этом одна и та же переменная может в разные моменты времени ссылаться на значения различных типов. Это позволяет писать гибкий и лаконичный код, но требует от разработчика повышенной осмотрительности в управлении состоянием программы.
То есть, переменная может быть сначала одного типа, а затем другого:
local x = 42 -- x ссылается на значение типа number
x = "hello" -- теперь x ссылается на значение типа string
x = true -- теперь x — булево значение
Разбор:
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Подобное поведение исключает необходимость явного объявления типов переменных и обеспечивает высокую степень метапрограммирования, однако делает статический анализ типов невозможным без использования внешних инструментов (например, Lua LSP или MoonScript).
Типы данных в Lua
В Lua определено восемь базовых типов данных, каждый из которых представляет собой самостоятельную категорию значений. Они могут быть классифицированы следующим образом:
nil
nil — тип, имеющий единственный экземпляр nil. Используется для обозначения отсутствия значения. Переменная, которой не присвоено значение, по умолчанию содержит nil. Это аналог null в других языках.
Такое определение может сильно запутать. Чтобы понять, нужно запомнить - в Lua нет "неинициализированных переменных" в классическом смысле, и если переменной ничего не присвоено, она просто содержит nil - то есть, "ничего".
Соответственно, у типа nil есть только одно возможное значение — сам nil. Любое выражение, результат которого "ничего", возвращает nil. В условиях nil считается ложным, вместе с false — это единственные два ложных значения в Lua. Всё остальное (включая 0, пустую строку "", пустую таблицу ) — истинно. И если вы объявите переменную:
local x
print(x)
Разбор:
- Ключевые вызовы в фрагменте:
print(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
То результат функции print(x) будет nil, то есть "ничего".
nil можно использовать для удаления элементов из таблиц:
local t = {a = 1, b = 2}
t.a = nil -- удаляем ключ 'a'
print(t.a) --> nil
Разбор:
- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Вы наверное задались вопросом - о каких таблицах идёт речь? Запомните - ещё вернёмся. Давайте сначала разберём прочие виды данных.
boolean
boolean — логический тип, допускающий два значения: true и false. В контексте условий любое значение, кроме nil и false, интерпретируется как истинное.
local active = true
local paused = false
if active and not paused then
print("Работает")
end
Разбор:
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. - Ключевые вызовы в фрагменте:
print(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Здесь мы видим переменные active и paused. Обратите внимание на проверку - она выполняется словами. В большинстве языков мы бы написали if active && !paused, используя символ && как "и", или "and", а также символ "!" как отрицание. Здесь же мы используем именно слова "and" и "not".
number
number — единый числовой тип. Снаружи type(x) всегда возвращает "number". В Lua 5.1–5.2 значения — double (IEEE 754). С Lua 5.3 внутри различаются представления integer и float, но для программиста это по-прежнему один тип number.
string
string — неизменяемые последовательности байтов, используемые для представления текста. Строки в Lua не имеют ограничений на содержание нулевых байтов и могут хранить произвольные бинарные данные. Интернирование строк позволяет эффективно сравнивать их по ссылке.
local name = "Иван"
local message = [[
Многострочная строка.
Можно использовать любые символы, включая "кавычки".
]]
print(#message) --> длина строки
Разбор:
localсоздаёт локальную переменную в текущем блоке; строка в кавычках — литерал типаstring.- Оператор
#для строк и последовательных таблиц возвращает длину (для строк — в байтах, не всегда в символах UTF-8). - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
function
function — тип, представляющий вызываемые объекты. Функции являются объектами первого класса — их можно передавать как аргументы, возвращать из других функций, хранить в переменных и структурах данных.
local add = function(a, b)
return a + b
end
print(add(3, 4)) --> 7
-- Функции — объекты первого класса:
local ops = {add = add}
print(ops.add(2, 3)) --> 5
Разбор:
returnзавершает функцию и может вернуть несколько значений через запятую.- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Но о функциях мы будем говорить отдельно.
table
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
table — единственная составная структура данных в Lua. Таблицы реализуют ассоциативные массивы и служат основой для построения массивов, словарей, объектов, модулей и других абстракций. Подробнее рассматривается ниже.
local user = {
name = "Bob",
age = 25,
hobbies = {"chess", "coding"}
}
print(user.name) --> Bob
print(user.hobbies[1]) --> chess
Разбор:
- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Здесь таблицей является user. Таблица является универсальным контейнером. В большинстве объектно-ориентированных языков такой тип данных называется объектом, но в Lua это таблицы.
Таблица — динамический гетерогенный ассоциативный массив (пары "ключ → значение"). Ключом может быть любое значение, кроме nil; запись t[k] = nil удаляет элемент. Одна и та же структура выражает разные идеи данных:
-- массив (индексы с 1 по умолчанию)
local days = {"пн", "вт", "ср"}
-- запись / «структура»
local person = {tabnum = 123, fio = "Иванов И.И.", salary = 25800.45}
-- множество (ключ = элемент, значение = true)
local workDays = {["пн"] = true, ["вт"] = true}
workDays["ср"] = nil -- убрали «среду» из множества
Подробные операции — в Работа со строками, таблицами и файлами и Метатаблицы.
userdata
userdata — тип, предназначенный для хранения произвольных данных, определяемых C-кодом. Используется преимущественно при расширении Lua через C API. Существует два вида: light userdata (указатель на C-объект) и full userdata (выделенная память, управляемая сборщиком мусора).
Используется при интеграции с C (Си). К примеру, это может быть хранение указателя на C-структуру:
/* В C-расширении: */
lua_pushlightuserdata(L, my_pointer);
Разбор:
lua_pushlightuserdataкладёт в стек Lua "лёгкий" userdata — по сути сырой указательvoid*из C-кода.L— указатель на состояние виртуальной машины; через него C-функция общается со стеком Lua.my_pointer— адрес объекта в памяти C; Lua не управляет этой памятью и не освобождает её сборщиком мусора.- Такой тип удобен, когда C-сторона сама контролирует время жизни структуры (движок, драйвер, буфер).
-- В Lua: light userdata — неизменяемый указатель из C
-- Нельзя менять из Lua, только передавать обратно в C-функции
Разбор:
- это поясняющий комментарий: в Lua нет полей у light userdata, только "прозрачная" ссылка.
- Из скрипта значение можно сохранить в переменную и передать обратно в C-функцию, которая знает, как интерпретировать указатель.
- Попытка обращаться к userdata как к таблице (
obj.field) приведёт к ошибке — тип не поддерживает индексацию без метатаблицы.
thread
thread — представляет поток выполнения (coroutine). Не следует путать с системными потоками; это легковесные сопрограммы, управляемые на уровне интерпретатора Lua.
local co = coroutine.create(function()
for i = 1, 3 do
print(i)
coroutine.yield()
end
end)
coroutine.resume(co) --> 1
coroutine.resume(co) --> 2
coroutine.resume(co) --> 3
Разбор:
- Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. - API
coroutine.*управляет сопрограммами —createсоздаёт,resumeзапускает/продолжает,yieldприостанавливает. - Ключевые вызовы в фрагменте:
coroutine.create(),coroutine.resume(),coroutine.yield(),print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Определить тип значения можно с помощью встроенной функции type(), которая возвращает строку с именем типа:
print(type(42)) --> number
print(type(nil)) --> nil
print(type(function() end)) --> function
Разбор:
type(x)возвращает строку с именем типа значения (number,string,table,functionи т.д.).- Ключевые вызовы в фрагменте:
print(),type(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Особое внимание заслуживает тип number. В Lua 5.1 и 5.2 все числовые значения реализованы исключительно как double-значения, что означает, что даже целые числа хранятся в формате с плавающей запятой. Это приводит к некоторым нетривиальным последствиям, таким как потеря точности при работе с очень большими целыми числами, невозможность различать целые и вещественные числа на уровне типа, и автоматическое округление в арифметических операциях, если результат не может быть точно представлен.
Начиная с Lua 5.3, была внедрена поддержка целочисленного внутреннего представления. Теперь тип number может хранить как 64-битные целые (int64), так и double-вещественные числа. Lua автоматически выбирает наиболее подходящее представление, а при необходимости выполняет преобразования.
Но это всё внутренняя оптимизация, с точки зрения языка number остаётся единым типом. Программист не может объявить переменную как "целое число", и проверка типа не различает подтипы.
Переменные
Переменные в Lua не имеют фиксированного типа и не требуют явного объявления. Любая переменная создаётся в момент первого присваивания. По области видимости переменные делятся на три категории:
Локальные переменные — объявляются с помощью ключевого слова local. Имеют блочную область видимости и рекомендуются к использованию по умолчанию. Они доступны только внутри блока, где объявлены (будь то цикл, функция, условие). Если они выходят из области видимости, то уничтожаются сборщиком мусора автоматически.
local function greet()
local name = "Alice"
print(name) -- работает
end
greet()
print(name) -- ошибка: name is nil (локальная вне функции недоступна)
Разбор:
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.- Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Здесь мы видим переменную с именем name - она и будет локальной, доступной только в пределах функции greet().
Глобальные переменные — создаются при первом присваивании без local. Хранятся в глобальной таблице _G. Использование глобальных переменных считается антипаттерном в крупных проектах.
Такие переменные доступны из любого места программы, и очевидно, что это загрязняет глобальное пространство имён, усложняя отладку, поэтому, если нет нужды, лучше создавать именно локальные переменные.
x = 100 -- глобальная переменная
_G.y = 200 -- то же самое, что y = 200
print(_G.x) --> 100
Разбор:
- Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Для объявления глобальной переменной не нужно никаких ключевых слов, просто написать имя и присвоить значение через символ "=". Тип данных тоже писать не нужно.
Это отличный пример работы динамической неявной типизации - программисту не нужно явно указывать тип данных, за него всё сделает язык.
Управляющие переменные циклов — например, i в for i = 1, 10 do ... end — являются локальными по умолчанию.
Параллельное присваивание
Оператор = поддерживает параллельное присваивание (идея из языка CLU): все выражения справа вычисляются до записи в переменные слева.
x, y = y, x -- обмен без временной переменной
a, b, c = 1, 2 -- c получит nil (значений меньше)
local u, v, w = f() -- w = nil, если f вернула два значения
Число переменных и число значений не обязаны совпадать: лишние значения отбрасываются, недостающие переменные получают nil. Это удобнее жёстких правил в ряде других языков.
Логика и "тернарный" приём
Ложными в условиях считаются только nil и false; 0, "" и {} — истинны. Операторы and / or короткозамкнутые и возвращают последнее вычисленное подвыражение, а не обязательно true/false:
-- выбор меньшего аргумента через f (если f(a) не nil/false)
local r = (a < b) and f(a) or f(b)
Пример:
x = 10 -- глобальная переменная
local y = 20 -- локальная переменная
Разбор:
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Отсутствие объявления типа и автоматическое создание глобальных переменных могут привести к ошибкам из-за опечаток. Поэтому в производственных средах рекомендуется использовать режим строгой проверки (например, через библиотеку strict.lua) или статические анализаторы.
Существует библиотека (или фрагмент кода) под названием strict.lua — это стороннее средство, используемое для включения строгой проверки использования переменных. Она работает через метатаблицы и отслеживание доступа к глобальной таблице _G. Если вы читаете или записываете в необъявленную глобальную переменную — выбрасывается ошибка. На практике такие вещи внедряют в крупные проекты (например, в играх на Love2D или в движках вроде Neovim), где важно контролировать состояние глобального пространства имён.
И как раз-таки рекомендуется использовать strict-режим, чтобы избежать случайных глобальных переменных.
Преобразование и работа с типами
Преобразование типов
Lua поддерживает автоматическое неявное преобразование между строками и числами в соответствующих контекстах. Это удобно, но требует осторожности.
При арифметических операциях строковые значения, содержащие корректные числовые литералы, автоматически преобразуются в числа:
print("10" + 1) --> 11
Разбор:
- Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
При конкатенации (..) числа автоматически преобразуются в строки:
print(42 .. " is the answer") --> "42 is the answer"
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Неудачные попытки преобразования приводят к ошибкам:
print("hello" + 1) -- ошибка: attempt to perform arithmetic on a string value
Разбор:
- Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Для явного преобразования используются функции:
- tonumber(s) — преобразует строку в число; возвращает nil, если преобразование невозможно.
- tostring(v) — преобразует значение любого типа в строку.
Пользовательские правила преобразования могут быть заданы через метатаблицы (например, метаметоды __tostring и __tonumber).
Работа с типами
Центральным элементом системы типов Lua является тип table, являющийся единственным механизмом для создания составных данных. Таблица представляет собой ассоциативный массив, то есть отображение ключей на значения. Ключи и значения могут быть любого типа, кроме nil.
Таблицы часто используются как упорядоченные последовательности, аналогичные массивам в других языках. По соглашению, такие таблицы используют целочисленные ключи, начинающиеся с 1 (в отличие от языков с нулевой индексацией):
local arr = {"apple", "banana", "cherry"}
print(arr[1]) --> "apple"
Разбор:
- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Функция #arr возвращает длину последовательности (количество элементов до первого nil). Однако необходимо помнить, что Lua не гарантирует плотность массива, и пропущенные индексы могут нарушить работу оператора длины.
Оператор # (или функция table.getn() в старых версиях) пытается определить длину "последовательности" — то есть количество элементов, идущих подряд с индекса 1. Но только до первого nil.
"Нормальный" массив:
local arr = {10, 20, 30}
print(#arr) --> 3
Разбор:
- Оператор
#для строк и последовательных таблиц возвращает длину (для строк — в байтах, не всегда в символах UTF-8). - Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Массив с "дырой":
local arr = {10, 20, nil, 40}
print(#arr) --> 2 (!!!)
Разбор:
- Оператор
#для строк и последовательных таблиц возвращает длину (для строк — в байтах, не всегда в символах UTF-8). - Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
То есть, элемент 40 игнорируется, потому что между 2 и 4 есть nil, и на третьем шаге будет конец последовательности.
Таблицы реализованы как хэш-таблицы с возможностью хранения упорядоченной последовательности (массивная часть) и хэшированной части (ассоциативная). Это позволяет эффективно комбинировать доступ по индексу и по ключу. Производительность таблиц зависит от равномерности распределения хэшей и размера структуры.
Упорядоченная последовательность - это таблица, используемая как массив — ключи — целые числа, начинающиеся с 1, идут подряд.
local fruits = {"apple", "banana", "orange"}
-- эквивалентно:
-- fruits[1] = "apple"
-- fruits[2] = "banana"
-- fruits[3] = "orange"
Разбор:
- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Здесь важна упорядоченность и непрерывность индексов.
Ассоциативные массивы (словари) являются структурой, где каждому значению соответствует ключ, и доступ к данным осуществляется по ключу, а не по позиции. В Lua любая таблица может быть ассоциативным массивом.
Таблицы одинаково хорошо работают как словари (hash maps), где ключами могут быть строки, числа или даже другие таблицы:
local person = {
name = "Alice",
age = 30,
["city"] = "Moscow"
}
print(person["name"]) --> "Alice"
print(person.age) --> 30
Разбор:
- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Здесь синтаксис t.key эквивалентен t["key"], если ключ является допустимым идентификатором.
Смешанные ключи - важная часть, касающаяся словарей. В одной таблице можно использовать разные типы ключей одновременно. Ключи разных типов не конфликтуют.
Особенность таблиц — возможность смешивать различные типы ключей в одной структуре:
local t = {}
t[1] = "first"
t["hello"] = "world"
t[{}] = 42 -- ключ — анонимная таблица
t[true] = "yes"
Разбор:
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Каждый ключ уникален — два разных объекта (например, две разные таблицы) никогда не считаются равными, даже если они содержат одинаковые данные.
Таблицы поддерживают механизм метатаблиц, позволяющий переопределять поведение операций (арифметика, сравнение, индексация и т. д.) через специальные метаметоды. Это делает таблицы основой для реализации ООП, операторной перегрузки и доменных DSL.
local mt = {
__add = function(a, b)
return setmetatable({value = a.value + b.value}, mt)
end
}
local a = setmetatable({value = 10}, mt)
local b = setmetatable({value = 20}, mt)
local c = a + b
print(c.value) --> 30
Разбор:
returnзавершает функцию и может вернуть несколько значений через запятую.setmetatableсвязывает таблицу с метатаблицей; поля__…задают перехват операций (индекс, арифметика, вызов).- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
print(),setmetatable(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.