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

Работа со строками, таблицами и файлами

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

Работа со строками, таблицами и файлами

Что такое строка и как она устроена

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

Внутреннее представление строки в Lua представляет собой массив байтов. Каждый символ в строке занимает один байт. Для стандартного ASCII-диапазона это соответствует одному символу на один байт. При работе с Unicode-символами (например, кириллицей, эмодзи или иероглифами) ситуация усложняется: один символ может занимать от одного до четырех байт в зависимости от кодировки. Lua по умолчанию использует UTF-8 для строк, что позволяет корректно обрабатывать символы практически всех письменностей мира.

Система управления памятью Lua автоматически отслеживает количество ссылок на каждую созданную строку. Если несколько переменных ссылаются на одну и ту же строковую литерал, система памяти объединяет их в единый объект. Это явление называется интернированием строк (string interning). Интернирование экономит память и ускоряет сравнение строк, так как достаточно проверить равенство указателей на объекты, а не перебирать все символы.

local str1 = "Привет"
local str2 = "Привет"
print(str1 == str2) -- true
-- str1 и str2 указывают на один и тот же объект в памяти

Строки в Lua могут быть заданы двумя способами: с помощью одинарных кавычек '...' или двойных кавычек "...". Оба способа полностью эквивалентны. Разница заключается лишь в удобстве использования при наличии внутри строки соответствующих кавычек. Если строка содержит двойные кавычки, её удобно заключить в одинарные, и наоборот.

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

  • \n — перевод строки;
  • \r — возврат каретки;
  • \t — горизонтальная табуляция;
  • \\ — обратная косая черта;
  • \" — двойная кавычка;
  • \' — одинарная кавычка.

Lua также поддерживает шестнадцатеричное представление символов через последовательность \xHH, где HH — два шестнадцатеричных символа. Это позволяет точно задать любой байт в диапазоне от 0 до 255.

local message = "Строка с переносом\nИ вторая строка."
local tab = "Текст\tс табуляцией"
local quote = "Он сказал: \"Привет\""
local hex = "\x41\x42\x43" -- ABC

Для работы с многострочными текстами, содержащими кавычки и сложные символы, в Lua существует специальный синтаксис с использованием длинных скобок [=[...]]=. Скобки могут иметь разную длину, но открывающая и закрывающая конструкции должны совпадать по количеству квадратных скобок. Внутри такого блока любые символы, включая кавычки и обратные слеши, воспринимаются буквально.

local multiline = [[
Это многострочная строка.
Она содержит кавычки "и '".
Здесь нет необходимости экранировать \ символы.
]]

Длина строки определяется количеством символов в ней. Функция # возвращает длину строки. В случае со строками, содержащими многобайтовые символы, оператор # возвращает количество байт, а не количество символов. Для получения точного количества символов необходимо использовать функции из модуля utf8.

local text = "Привет"
print(#text) -- выведет 6, так как каждый кириллический символ занимает 2 байта

Основные операции над строками

Конкатенация

Конкатенация — это процесс соединения двух или более строк в одну. В Lua для этой цели используется оператор .. (две точки). Оператор соединяет строки слева направо.

local first = "Hello"
local second = "World"
local result = first .. " " .. second
print(result) -- Hello World

При конкатенации участвуют не только строковые типы, но и другие типы данных. Lua автоматически выполняет приведение типов: числа, булевы значения и nil преобразуются в строковое представление. Число превращается в его десятичную запись, булево значение — в строку "true" или "false", а nil — в пустую строку.

local number = 42
local boolean = true
local nilValue = nil
print("Число: " .. number) -- Число: 42
print("Булево: " .. boolean) -- Булево: true
print("Nil: " .. nilValue) -- Nil:

Сравнение строк

Сравнение строк в Lua осуществляется операторами == (равно) и ~= (не равно). Сравнение происходит побайтово. Для латинского алфавита порядок символов соответствует их кодам в таблице ASCII. Для других алфавитов порядок определяется в соответствии с таблицей Unicode.

local a = "abc"
local b = "abd"
print(a < b) -- true, так как 'c' меньше 'd'
print(a == "abc") -- true
print(a ~= "xyz") -- true

Преобразование типов

Функция tostring() преобразует любое значение в строковый формат. Для чисел она возвращает их десятичное представление. Для таблиц, функций и кортежей она возвращает строку вида "table: 0x...", где указатель на адрес памяти.

print(tostring(123)) -- "123"
print(tostring(true)) -- "true"
print(tostring({1, 2})) -- "table: 0x..."

Функция tonumber() выполняет обратное преобразование: строка в число. Если строка не содержит валидного числового представления, функция возвращает nil.

print(tonumber("42")) -- 42
print(tonumber("abc")) -- nil

Поиск подстрок

Функция string.find(s, pattern) ищет первое вхождение шаблона в строке. Она возвращает индекс начала найденной подстроки и индекс её конца. Если подстрока не найдена, функция возвращает nil.

local text = "Lua Programming"
local start, finish = string.find(text, "Programming")
print(start, finish) -- 5 14

Аргумент pattern может быть простой строкой или регулярным выражением. Если передан просто текст, поиск выполняется как точное совпадение.

local text = "Hello World"
print(string.find(text, "World")) -- 7 11

Функция string.sub(s, i, j) извлекает подстроку из строки s, начиная с индекса i и заканчивая индексом j. Индексация начинается с единицы. Если второй индекс опущен, подстрока берется до конца строки. Отрицательные индексы считаются с конца строки.

local text = "Lua"
print(string.sub(text, 1, 2)) -- Lu
print(string.sub(text, -1)) -- a

Методы работы со строками в Lua

Форматирование строк

Функция string.format(fmt, ...) формирует строку на основе формата и списка аргументов. Формат строки начинается со знака % и указывает тип данных и способ вывода.

Основные спецификаторы формата:

  • %s — строка;
  • %d — целое число;
  • %f — число с плавающей точкой;
  • %x — число в шестнадцатеричном формате;
  • %% — сам знак процента.
local name = "Alice"
local age = 30
local greeting = string.format("Привет, %s! Тебе %d лет.", name, age)
print(greeting) -- Привет, Alice! Тебе 30 лет.

Можно задавать ширину поля и точность вывода. Например, %5d выводит число в поле шириной 5 символов, дополняя пробелами слева. %5.2f выводит число с плавающей точкой с шириной 5 и точностью 2 знака после запятой.

print(string.format("%5d", 42)) -- " 42"
print(string.format("%.2f", 3.14159)) -- "3.14"

Разбиение строк

Функция string.split отсутствует в стандартной библиотеке Lua, поэтому разбиение строк реализуется через циклы и функцию string.gmatch или string.match.

Функция string.gmatch(s, pattern) возвращает итератор, который проходит по всем вхождениям шаблона в строке. Это позволяет легко извлекать слова, разделенные пробелами или другими разделителями.

local text = "один два три"
for word in string.gmatch(text, "%S+") do
print(word)
end
-- Вывод:
-- один
-- два
-- три

Для разбиения строки по конкретному разделителю можно использовать цикл с функцией string.find.

function split(str, sep)
local parts = {}
local start = 1
local sepLen = #sep
while true do
local pos = string.find(str, sep, start, true)
if not pos then break end
table.insert(parts, string.sub(str, start, pos - 1))
start = pos + sepLen
end
table.insert(parts, string.sub(str, start))
return parts
end

local Данные = "apple,banana,cherry"
local fruits = split(Данные, ",")
for _, fruit in ipairs(fruits) do
print(fruit)
end

Замена подстрок

Функция string.gsub(s, pattern, repl) заменяет все вхождения шаблона в строке на указанную замену. Возвращает новую строку и количество выполненных замен.

local text = "cat dog cat"
local new_text, count = string.gsub(text, "cat", "dog")
print(new_text) -- dog dog dog
print(count) -- 2

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

local text = "one two three"
local upper_text = string.gsub(text, "%w+", function(word)
return string.upper(word)
end)
print(upper_text) -- ONE TWO THREE

Приведение регистра

Функции string.upper(s) и string.lower(s) преобразуют строку соответственно в верхний и нижний регистр. Эти функции учитывают национальные особенности алфавита.

print(string.upper("hello")) -- HELLO
print(string.lower("HELLO")) -- hello

Удаление пробелов

Функции string.trim, string.ltrim и string.rtrim отсутствуют в стандартной библиотеке Lua. Их реализацию можно выполнить вручную или использовать регулярные выражения.

local text = " hello world "
local trimmed = string.gsub(text, "^%s*(.-)%s*$", "%1")
print(trimmed) -- hello world

Функция string.gsub с паттерном ^%s* удаляет пробелы в начале строки, а (.-)%s*$ — в конце.


Проверка содержимого

Функции string.match(s, pattern) и string.find(s, pattern) позволяют проверять наличие определенных конструкций в строке. string.match возвращает первое найденное совпадение или nil.

local email = "user@example.com"
if string.match(email, "@") then
print("Строка содержит @")
end

Проверка на цифру:

if string.match("123", "^%d+$") then
print("Строка состоит только из цифр")
end

Что такое таблица и как она устроена

Таблица — это универсальная структура данных в Lua, представляющая собой ассоциативный массив. Таблица связывает ключи со значениями. Ключом может служить любой тип данных, кроме nil и NaN. Значением может быть любой тип данных, включая другую таблицу, функцию или ссылку на себя.

Таблица в Lua — это динамическая структура. Её размер меняется автоматически при добавлении или удалении элементов. Нет необходимости заранее выделять память или указывать максимальное количество элементов.

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

local t = {10, 20, 30}
print(t[1]) -- 10
print(t[2]) -- 20
print(t[3]) -- 30

Ключи в таблице могут быть любого типа. Строковые ключи часто используются для создания объектов или структур данных.

local person = {
name = "Ivan",
age = 25,
city = "Moscow"
}
print(person.name) -- Ivan

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

local company = {
name = "TechCorp",
departments = {
sales = { head = "Anna" },
it = { head = "Petr" }
}
}
print(company.departments.it.head) -- Petr

Основные операции с таблицами

Создание таблицы

Таблицу можно создать с помощью фигурных скобок {}. Элементы разделяются запятыми. Если элементы имеют имена (ключи), они записываются в виде key = value. Если ключи не указаны, они присваиваются автоматически, начиная с единицы.

local empty_table = {}
local numbers = {1, 2, 3, 4, 5}
local mixed = {name = "Test", value = 100, active = true}

Добавление и удаление элементов

Элементы добавляются в таблицу путем присваивания значения по ключу.

local t = {}
t[1] = "first"
t["second"] = 2
t[3] = {nested = true}

Удаление элемента осуществляется путем присваивания ему значения nil.

t["second"] = nil

Перебор таблицы

Для перебора элементов таблицы используются итераторы. Наиболее распространенный итератор — pairs(). Он проходит по всем парам ключ-значение в произвольном порядке.

local t = {a = 1, b = 2, c = 3}
for key, value in pairs(t) do
print(key, value)
end

Для перебора только числовых ключей, расположенных последовательно, используется итератор ipairs(). Он останавливается при встрече первого nil.

local t = {10, 20, 30, nil, 40}
for index, value in ipairs(t) do
print(index, value)
end
-- Вывод:
-- 1 10
-- 2 20
-- 3 30
-- Цикл завершается, так как элемент 4 равен nil

Изменение размера таблицы

Функция table.insert(t, [pos], value) вставляет элемент в таблицу. Если указан индекс pos, элемент вставляется перед существующим элементом. Если индекс не указан, элемент добавляется в конец.

local t = {1, 2}
table.insert(t, 3)
print(t[3]) -- 3
table.insert(t, 2, 10)
print(t[2]) -- 10
print(t[3]) -- 2

Функция table.remove(t, [pos]) удаляет элемент из таблицы. По умолчанию удаляется последний элемент.

local t = {1, 2, 3}
table.remove(t)
print(t[2]) -- 2
table.remove(t, 1)
print(t[1]) -- 2

Сортировка таблицы

Функция table.sort(t, comp) сортирует элементы таблицы. Аргумент comp — функция сравнения, которая принимает два элемента и возвращает true, если первый должен идти перед вторым.

local numbers = {5, 2, 8, 1, 9}
table.sort(numbers)
print(table.concat(numbers, ", ")) -- 1, 2, 5, 8, 9

Для сортировки таблиц по определенным полям используется пользовательская функция сравнения.

local people = {
{name = "Anna", age = 30},
{name = "Boris", age = 25},
{name = "Cyril", age = 35}
}

table.sort(people, function(a, b)
return a.age < b.age
end)

for _, p in ipairs(people) do
print(p.name, p.age)
end

Объединение таблиц

Функция table.concat(t, sep, i, j) объединяет элементы таблицы в строку, используя разделитель sep. Диапазон элементов указывается параметрами i и j.

local words = {"Hello", "world"}
print(table.concat(words, " ")) -- Hello world

Для объединения двух таблиц можно использовать цикл и оператор присваивания.

local t1 = {1, 2}
local t2 = {3, 4}
for _, v in ipairs(t2) do
table.insert(t1, v)
end
print(table.concat(t1, ", ")) -- 1, 2, 3, 4

Поиск элементов

Функция table.find(t, value) отсутствует в стандартной библиотеке. Поиск выполняется вручную с помощью цикла.

function find(t, value)
for k, v in pairs(t) do
if v == value then
return k
end
end
return nil
end

local t = {a = 10, b = 20}
print(find(t, 20)) -- b

Что такое файл и как он устроен

Файл — это именованный набор данных, хранящийся на носителе информации. В Lua работа с файлами осуществляется через встроенную библиотеку io. Библиотека предоставляет функции для открытия, чтения, записи и закрытия файлов.

Файл в Lua представляется объектом типа file. Объект файла содержит информацию о текущем положении курсора, режиме доступа и пути к файлу. Все операции с файлом выполняются через методы этого объекта или глобальные функции.

Режимы доступа к файлам:

  • "r" — чтение (файл должен существовать);
  • "w" — запись (файл создается или очищается);
  • "a" — добавление (файл создается или данные дописываются в конец);
  • "r+" — чтение и запись (файл должен существовать);
  • "w+" — чтение и запись (файл создается или очищается);
  • "a+" — чтение и добавление (файл создается или данные дописываются).
local file = io.open("test.txt", "r")
if file then
print(file:read())
file:close()
else
print("Файл не открыт")
end

Открытие файла

Функция io.open(filename, mode) открывает файл и возвращает объект файла или nil в случае ошибки. Второй аргумент mode указывает режим доступа.

local file = io.open("Данные.txt", "w")
if file then
file:write("Привет, мир!")
file:close()
else
print("Ошибка открытия файла")
end

Функция io.input(filename) устанавливает текущий входной файл по умолчанию. Все последующие вызовы io.read() будут читать из этого файла.

io.input("input.txt")
local line = io.read()

Функция io.output(filename) устанавливает текущий выходной файл по умолчанию. Все последующие вызовы io.write() будут писать в этот файл.

io.output("output.txt")
io.write("Текст для записи")

Чтение из файла

Метод file:read([size]) читает данные из файла. Аргумент size определяет формат чтения:

  • number — прочитать n байт;
  • "line" — прочитать одну строку (до перевода строки);
  • "lines" — вернуть итератор для чтения всех строк;
  • nil — прочитать следующую строку.
local file = io.open("text.txt", "r")
if file then
local line = file:read()
print(line)
file:close()
end

Для чтения всего содержимого файла можно использовать итератор.

local file = io.open("text.txt", "r")
if file then
for line in file:lines() do
print(line)
end
file:close()
end

Запись в файл

Метод file:write(...) записывает данные в файл. Аргументы могут быть строками или числами.

local file = io.open("log.txt", "a")
if file then
file:write("Запись №1\n")
file:write("Запись №2\n")
file:close()
end

Позиционирование в файле

Метод file:seek([whence][, offset]) перемещает курсор чтения/записи в файле. Аргумент whence указывает точку отсчета:

  • "set" — начало файла (по умолчанию);
  • "cur" — текущая позиция;
  • "end" — конец файла.

Аргумент offset — смещение в байтах.

local file = io.open("Данные.bin", "r+b")
if file then
file:seek("set", 0) -- Начало файла
local content = file:read("*all")

file:seek("cur", 10) -- Смещение на 10 байт вперед
local part = file:read(5)

file:seek("end", -5) -- 5 байт от конца
local tail = file:read("*all")

file:close()
end

Закрытие файла

Метод file:close() закрывает файл и освобождает ресурсы. После закрытия файл нельзя использовать для чтения или записи.

local file = io.open("test.txt", "w")
file:write("Текст")
file:close()

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

Все операции с файлами могут завершиться ошибкой. Рекомендуется проверять результат вызова io.open. Если возвращается nil, следует обработать ошибку.

local file, err = io.open("nonexistent.txt", "r")
if not file then
print("Ошибка:", err)
end

Пример 1: Парсинг CSV-файла

function parse_csv(filename)
local file = io.open(filename, "r")
if not file then
return nil, "Не удалось открыть файл"
end

local rows = {}
for line in file:lines() do
local row = {}
for field in string.gmatch(line, "[^,]+") do
table.insert(row, field)
end
table.insert(rows, row)
end

file:close()
return rows
end

local Данные, error = parse_csv("users.csv")
if Данные then
for _, user in ipairs(Данные) do
print(user[1], user[2])
end
else
print(error)
end

Пример 2: Сохранение и загрузка конфигурации

function save_config(filename, config)
local file = io.open(filename, "w")
if not file then
return false
end

for key, value in pairs(config) do
file:write(key, "=", tostring(value), "\n")
end

file:close()
return true
end

function load_config(filename)
local config = {}
local file = io.open(filename, "r")
if not file then
return nil
end

for line in file:lines() do
local key, value = string.match(line, "(%w+)=(.*)")
if key and value then
config[key] = tonumber(value) or value
end
end

file:close()
return config
end

local settings = {
width = 800,
height = 600,
fullscreen = true
}

save_config("settings.cfg", settings)
local loaded = load_config("settings.cfg")
print(loaded.width, loaded.fullscreen)

Пример 3: Обработка больших файлов

function process_large_file(input_file, output_file)
local input = io.open(input_file, "r")
local output = io.open(output_file, "w")

if not input or not output then
if input then input:close() end
if output then output:close() end
return false
end

for line in input:lines() do
local processed = string.gsub(line, "%s+", " ")
output:write(processed, "\n")
end

input:close()
output:close()
return true
end

process_large_file("input.log", "output.log")

Пример 4: Работа с бинарными файлами

function write_binary(filename, Данные)
local file = io.open(filename, "wb")
if not file then
return false
end

file:write(Данные)
file:close()
return true
end

function read_binary(filename)
local file = io.open(filename, "rb")
if not file then
return nil
end

local Данные = file:read("*all")
file:close()
return Данные
end

local binary_data = string.char(0x00, 0xFF, 0xAA)
write_binary("Данные.bin", binary_data)
local read_data = read_binary("Данные.bin")
print(read_data)

Поддержка UTF-8 в Lua

Lua 5.3 и выше имеет встроенную поддержку UTF-8 через модуль utf8. Модуль предоставляет функции для работы с кодировкой Unicode.

Функция utf8.len(s) возвращает количество символов в строке, учитывая многобайтовую кодировку.

local text = "Привет"
print(utf8.len(text)) -- 6

Функция utf8.codes(s) возвращает итератор, выдающий коды символов в строке.

for code in utf8.codes("A") do
print(code) -- 65
end

Функция utf8.char(...) создает строку из кодов символов.

print(utf8.char(65, 66, 67)) -- ABC

Функция utf8.offset(s, n) возвращает позицию байта, соответствующего n-му символу в строке.

local text = "Привет"
print(utf8.offset(text, 1)) -- 1 (первый символ)
print(utf8.offset(text, 2)) -- 3 (второй символ)

Обработка ошибок кодировки

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

local file = io.open("Данные.txt", "r")
if file then
local content = file:read("*all")
-- Проверка на некорректные байты
if string.find(content, "[\128-\191]") then
print("Обнаружены некорректные символы")
end
file:close()
end

Буферизация ввода-вывода

Для повышения производительности при работе с большими объемами данных рекомендуется использовать буферизацию. Функция io.lines() автоматически буферизирует ввод, что ускоряет чтение.

-- Эффективный способ чтения большого файла
for line in io.lines("large_file.txt") do
-- обработка строки
end

Избегание лишних копий строк

При конкатенации множества строк в цикле лучше использовать таблицу и table.concat, чем оператор ...

-- Неэффективно
local result = ""
for i = 1, 1000 do
result = result .. i
end

-- Эффективно
local parts = {}
for i = 1, 1000 do
table.insert(parts, tostring(i))
end
local result = table.concat(parts, "")

Использование потоков вместо файлов

Для временного хранения данных в памяти предпочтительнее использовать таблицы, а не файлы. Файловый ввод-вывод медленнее работы с памятью.

local cache = {}
for i = 1, 10000 do
cache[i] = generate_data(i)
end
-- Дальнейшая работа с таблицей

Паттерн "Фабрика файлов"

Создание абстрактного слоя для работы с файлами позволяет легко менять реализацию.

local FileFactory = {}

function FileFactory.create(filename, mode)
local file, err = io.open(filename, mode)
if not file then
error("Ошибка открытия файла: " .. err)
end
return setmetatable({
_file = file,
_filename = filename,
_mode = mode
}, {
__index = {
read = function(self) return self._file:read() end,
write = function(self, Данные) self._file:write(Данные) end,
close = function(self) self._file:close() end
}
})
end

local f = FileFactory.create("test.txt", "w")
f:write("Тест")
f:close()

Паттерн "Логгер"

Централизованная система логирования с возможностью выбора уровня и формата.

local Logger = {}

function Logger.init(level, file)
local log_file = io.open(file, "a")
Logger.level = level
Logger.file = log_file
end

function Logger.log(level, message)
if level >= Logger.level then
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
Logger.file:write(string.format("[%s] [%s] %s\n", timestamp, level, message))
end
end

Logger.init(1, "app.log")
Logger.log(1, "Информационное сообщение")
Logger.log(2, "Предупреждение")

Паттерн "Кэш конфигурации"

Хранение часто используемых данных в памяти для ускорения доступа.

local ConfigCache = {}

function ConfigCache.load(filename)
if ConfigCache.Данные then
return ConfigCache.Данные
end

local file = io.open(filename, "r")
if not file then
return {}
end

local config = {}
for line in file:lines() do
local key, value = string.match(line, "(%w+)=(.*)")
if key then
config[key] = tonumber(value) or value
end
end

file:close()
ConfigCache.Данные = config
return config
end

local cfg = ConfigCache.load("config.txt")

См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).

Освоение главы0%