Lua примеры. Основы Lua

Lua gives you the power; you build the mechanisms.
// Roberto Ierusalimsky


Введение

Lua - язык программирования, предназначенный для встраивания в другие приложения чтобы дать их пользователям возможность писать конфигурационные скрипты и высокоуровневые сценарии. Lua поддерживает процедурный, объектный и функциональный стили программирования, но является в то же время простым языком. Интерпретатор Lua написан на ANSI-C и представляет собой библиотеку, которую можно подключить к любой программе. В этом случае управляющая программа может вызвать библиотечные функции для выполнения участка кода на Lua и работы с данными, определенными в этом коде. Также управляющая программа может регистрировать собственные функции таким образом, что их можно будет вызывать из кода на Lua . Последняя возможность позволяет использовать Lua как язык, который можно адаптировать к произвольной области применения. Другое применение Lua - написание простых независимых скриптов. Для этой цели имеется простой интерпретатор Lua , использующая эту библиотеку для выполнения кода, вводимого с консоли или из файла.

Лексические соглашения

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

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

В идентификаторах различается верхний и нижний регистры букв.

Строковые литералы можно заключать в одинарные или двойные кавычки. В них можно использовать следующие специальные последовательности символов:

\n перевод строки (LF = 0x0a) \a bell \r возврат каретки (CR = 0x0d) \b backspace \t табуляция \f form feed \\ символ обратной косой черты \v вериткальная табуляция \" кавычка \[ левая квадратная скобка \" апостроф \] правая квадратная скобка \ddd символ с кодом ddd (десятичным) \0 символ с кодом 0

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

Строковые литералы можно также заключать в двойные квадратные скобки [[....]] . В этом случае литерал может быть определен на нескольких строках (символы перевода строки включаются в строковый литерал) и в нем не интерпретируются специальные последовательности символов.

Если непосредственно после символов "[[" идет перевод строки, то он не включается в строковый литерал.

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

В числовых константах можно указывать необязательную дробную часть и необязательный десятичный порядок, задаваемый символами "e" или "E" . Целочисленные числовые константы можно задавать в 16-ричной системе, используя префикс 0x .

Комментарий начинается символами "--" (два минуса подряд) и продолжается до конца строки. Если непосредственно после символов "--" идут символы "[[" , то комментарий является многострочным и продолжается до символов "]]" . Многострочный комментарий может содержать вложенные пары символов [[....]] . В качестве ограничителей многострочных комментариев кроме двойных квадратных скобок может также использоваться символ [===[ .... ]===] в котором между повторяющимися квадратными скобками расположено произвольное число знаков равенства (одинаковое для открывающего и закрывающего ограничителя). Строковая константа экранирует символы начала комментария.

Если первая строка файла начинается с символа "#" , то она пропускается. Это позволяет использовать Lua как интерпретатор скриптов в Unix-подобных системах.

Типы данных

В Lua имеются следующие типы данных:

Nil пусто boolean логический number числовой string строковый function функция userdata пользовательские данные thread поток table ассоциативный массив

Тип nil соответствует отсутствию у переменной значения. Этому типу соответствует единственное значение nil .

Логический тип имеет два значения: true и false .

Значение nil рассматривается как false . Все остальные значения, включая число 0 и пустую строку, рассматриваются как логическое значение true .

Все числа представлены как вещественные числа двойной точности.

Строки представляют собой массивы 8-битных символов и могут содержать внутри себя символ с нулевым кодом. Все строки в Lua константные т.е. изменить содержимое существующей строки нельзя.

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

Тип userdata соответствует нетипизированному указателю, по которому могут быть расположеы произвольные данные. Программа на Lua не может непосредственно работать с такими данными (создавать, модифицировать их). Этому типу данных не соответствует никаких предопределенных операций кроме присваивания и сравнения на равенство. В то же время такие операции могут быть определены при помощи механизма метаметодов.

Тип thread соответствует независимо выполняемому потоку. Этот тип данных используется механизмом сопрограмм.

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

Тип объекта, сохраненного в переменной можно выяснить, вызвав функцию type() . Эта функция возвращает строку, содержащую каноническое название типа: "nil", "number", "string", "boolean", "table", "function", "thread", "userdata" .

Преобразования между числами и строками происходят автоматически в момент их использования в соответствующем контексте.

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

Можно также явно преобразовать объект в строку с помощью функции tostring() или в число с помощью функции tonumber() . Для большего контроля над процессом преобразования чисел в строки следует использовать функцию форматного преобразования.

Переменные

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

Переменная считается глобальной, если она явно не объявлена как локальная. Объявление локальных переменных может быть расположено в любом месте блока и может быть совмещено с их инициализацией:

Local x, y, z local a, b, c = 1, 2, 3 local x = x

При инициализации локальной переменной справа от знака равенства вводимая переменная еще не доступна и используется значение переменной, внешней по отношению к текущему блоку. Именно поэтому корректен пример в третьей строке (он демонстрирует часто используемую идиому языка).

Для локальных переменных интерпретатор использует лексические области видимости т.е. область действия переменной простирается от места ее описания (первого использования) и до конца текущего блока. При этом локальная переменная видима в блоках, внутренних по отношению к блоку, в котором она описана. Локальная переменная исчезает при выходе из области видимости. Если локальная переменная определена вне блока, то такая переменная исчезает по окончании исполнения этого участка кода, поскольку участок кода выполняется интерпретатором как безымянная функция. Инициализированная глобальная переменная существует все время функционирования интерпретатора.

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

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

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

Таблицы

Таблицы (тип table) соответствует ассоциативным массивам, которые можно индексировать любыми значениями кроме nil и которые могут одновременно содержать значения произвольных типов кроме nil . Элементы таблицы можно индексировать и объектами - таблицами, функциями и объектами типа userdata . Элементы массива, не получившие значения в результате присваивания, имеют по умолчанию значение nil .

Таблицы - основная структура данных в Lua . С их помощью представляются также структуры, классы и объекты. В этом случае используется индексирование строковым именем поля структуры. Поскольку элементом массива может быть функция, в структурах допускаются также и методы.

Для индексирования массивов используются квадратные скобки: array . Запись struct.field эквивалентна следующей записи: struct["field"] . Эта синтаксическая особенность позволяет использовать таблицы в качестве записей с поименованными полями.

Конструктор таблицы - это выражение, создающее и возвращающее новую таблицу. Каждое выполнение конструктора создает новую таблицу. Конструктор таблицы представляет собой заключенный в фигурные скобки список инициализаторов полей (возможно пустой), разделенных запятой или символом ";" (точка с запятой). Для инициализаторов полей допустимы следующие варианты:

Exp2 table = exp2 name = exp table["name"] = exp exp table[j] = exp

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

X = { len = 12, 11, 12, = 1123 }

После выпонения такого оператора поля таблицы получат следующие значения:

X["len"] = x.len = 12 x = 11 x = 12 x = 1123

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

После последнего инициализатора может идти необязательный символ-разделитель инициализаторов полей (запятая или точка с запятой).

Операции

Ниже перечислены основные операции:

Смена знака + - * / арифметика ^ возведение в степень == ~= равенство < <= > >= порядок not and or логика.. конкатенация строк # получение длины строки или массива

При применении арифметических операций строки, имеющие числовое значение, приводятся к нему. При конкатенации числовых значений они автоматически преобразуются в строки.

При сравнении на равенство не производится преобразование типов. Объекты разных типов всегда считаются различными.

Соответственно "0" ~= 0 , а при индексировании a и a["0"] соответствуют разным ячейкам массива. При сравнении на равенство/неравенство объектов производится сравнение ссылок на объекты. Равными оказываются переменные, ссылающиеся на один и тот же объект.

При выяснении порядка типы аргументов должны совпадать, т.е. числа сравниваются с числами, а строки - со строками.

Отношения равенства и порядка всегда дают в результате true или false т.е. логическое значение.

В логических операциях nil рассматривается как false , а все остальные значения, включая нулевое число и пустую строку - как true .

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

Действует следующая таблица приоритетов и ассоциативности операций:

^ not # -(unary) * / + - < > <= >= ~= == .. and or

Логические операции и связанные с ними идиомы

Оператор not всегда возвращает логическое значение, принимая аргумент произвольного типа (при этом только значение nil соответствует логическому значению false , остальные же трактуются как true). В отличие от него операторы and и or всегда возвращают один из своих аргументов. Оператор or возвращает свой первый аргумент, если его значение отлично от false и nil и второй аргумент в противном случае. Оператор and возвращает свой первый аргумент, если его значение равно false или nil и второй аргумент в противном случае. Такое поведение основано на том, что все значения, отличные от nil , трактуются как true .

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

X = x or v if x == nil then x = v end x = (e and a) or b if e ~= nil then x = a else x = b end

Первая идиома часто используется для присвоения неинициализированной переменной умалчиваемого значения. Вторая идиома эквивалентна C"шному оператору x = e ? a, b (здесь считается, что значение переменной a отлично от nil).

Операторы

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

Операторы можно (но не обязательно) разделять символом ";" .

Допускается множественное присваивание:

Var1, var2 = val1, val2

При этом производится выравнивание - лишние значения отбрасываются, а переменным, соответствующим недостающим значениям присваивается значение nil . Все выражения, входящие в правую часть множественного присваивания, вычисляются до самого присвоения.

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

X, y, z = f(); a, b, c, d = 5, f();

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

Ниже перечислены основные операторы:

Do ... end if ... then ... end if ... then ... else ... end if ... then ... elseif ... then ... end if ... then ... elseif ... then ... else ... end while ... do ... end repeat ... until ... for var = start, stop do ... end for var = start, stop, step do ... end return return ... break

Блок do ... end превращает последовательность операторов в один оператор и открывает новую область видимости, в которой можно определять локальные переменные.

В операторах if , while и repeat все значения выражения, отличные от false и nil трактуются как истинные.

Вот общая форма записи оператора if:

If ... then ... {elseif ... then ...} end

Оператор return может не содержать возвращаемых значений или содержать одно или несколько выражений (список). Поскольку блок кода выполняется как ананимная функция, возвращаемое значение может быть не только у функции, но и у произвольного блока кода.

Операторы return и break должны быть последними операторами в блоке (т.е. должны быть либо последними операторами в блоке кода, либо распологаться непосредственно перед словами end , else , elseif , until). Внутри блока необходимо использовать идиому do return end или do break end .

Оператор for выполняется для всех значений переменной цикла начиная от стартового значения и кончая финишным значением включительно. Третье значение, если оно задано, используется как шаг изменения переменной цикла. Все эти значения должны быть числовыми. Они вычисляются только однажды перед выполнением цикла. Переменная цикла локальна в этом цикле и не доступна вне его тела. Значение переменной цикла нельзя изменять внутри тела цикла. Вот псевдокод, демонстрирующий выполнение оператора for:

Do local var, _limit, _step = tonumber(start), tonumber(stop), tonumber(step) if not (var and _limit and _step) then error() end while (_step>0 and var<=_limit) or (_step<=0 and var>=_limit) do ... var = var + _step end end

Функции

Определение функции - это исполняемое выражение (конструктор функции), результатом вычисления которого является объект типа функция:

F = function(...) ... end

В скобках размещается список (возможно пустой) аргументов функции. Список аргументов функции может заканчиваться троеточием - в этом случае функция имеет переменное число аргументов. Между закрывающейся скобкой и оператором end размещается тело функции.

Для определения функции имеются следующие краткие формы:

Function fname(...) ... end fname = function(...) ... end local function fname(...) ... end local fname = function(...) ... end function x.fname(...) ... end x.fname = function(...) ... end function x:fname(...) ... end x.fname = function(self, ...) ... end

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

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

Для вызова функций имеются следующие краткие формы:

F{...} f({...}) f("...") f"..." f("") f"" f([[...]]) f[[...]] x:f(...) x.f(x, ...)

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

При вызове функции значения простых типов копируются в аргументы по значению, а для объектов в аргументы копируются ссылки. При вызове функции производится выравнивание числа аргументов - лишние значения отбрасываются, а аргументы, соответствующие недостающим значениям получают значение nil . Если в конце списка параметров стоит вызов функции, возвращающей несколько значений, то все они добавляются в список аргументов. Это поведение можно изменить, заключив вызов функции в круглые скобки. В этом случае из всех возвращаемых функцией значений используется только первое.

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

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

Function rename(arg) arg.new = arg.new or arg.old .. ".bak" return os.rename(arg.old, arg.new) end rename{ old = "asd.qwe" }

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

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

Имеется предопределенная функция unpack() , которая принимает массив, элементы которого проиндексированы с 1 , а возвращает все его элементы. Эта функция может быть использована для вызова функций, принимающих переменное число аргументов с динамическим формированием списка фактических аргументов. Обратная операция производится следующим образом:

List1 = {f()} -- создать список из всех значений, возвращенных функцией f() list2 = {...} -- создать список из всех значений, переданных в функцию с переменным числом аргументов

Переменное число возвращаемых значений, правило дополнения списков и возможность передавать в функцию не все формальные параметры могут взаимодействовать нетривиальным образом. Часто функция (например foo()) возвращает ответ при нормальном завершении и nil и сообщение об ошибке при ненормальном. Функция assert(val, msg) генерирует ошибку с сообщением message (вызывая функцию error(msg)) если val имеет значение false или nil и возвращает значение val в противном случае. Тогда оператор

V = assert(foo(), "message")

в случае успеха присваивает переменной v значение, возвращаемое функцией foo() . В этом случае foo() возвращает одно значение, а assert() получает параметр msg , равный nil . В случае ошибки функция assert() получает nil и сообщение об ошибке.

Итераторы

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

For v_1, v_2, ..., v_n in explist do ... end

Число переменных в списке v_1, ..., v_n может быть произвольным и не обязано соответствовать числу выражений в списке explist . В роли explist обычно выступает вызов фунции-фабрики итераторов. Такая функция возвращает функцию-итератор, состояние и начальное значение управляющей переменной цикла. Итератор интерпретируется следующим образом:

Do local f, s, v_1 = explist local v_2, ... , v_n while true do v_1, ..., v_n = f(s, v_1) if v_1 == nil then break end ... end end

На каждом шаге значения всех переменных v_k вычисляются путем вызова функции-итератора. Значение управляющей переменной v_1 управляет завершением цикла - цикл завершается как только функция-итератор возвратит nil как значение для переменной var_1 .

Фактически итерациями управляет переменная v_1 , а остальные переменные можно рассматривать как «хвост», возвращаемый функцией-итератором:

Do local f, s, v = explist while true do v = f(s, v) if v == nil then break end ... end end

Оператор, в котором вызывается функция-фабрика, интерпретируется как обычный оператор присваивания т.е. функция-фабрика может возвращать произвольное количество значений.

Итераторы без внутреннего состояния

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

Function iter(a, i) i = i + 1 local v = a[i] if v then return i, v else return nil end end function ipairs(a) return iter, a, 0 end

Итераторы, хранящие состояние в замыкании

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

Function ipairs(a) local i = 0 local t = a local function iter() i = i + 1 local v = t[i] if v then return i, v else return nil end end return iter end

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

Стандартные итераторы

Чаще всего итераторы применяются для обхода элементов таблиц. Для этого существует несколько предопределенных функций-фабрик итераторов. Фабрика pairs(t) возвращает итератор, дающий на каждом шаге индекс в таблице и размещенное по этому индексу значение:

For idx, val in pairs(tbl) do ... end

На самом деле этот итератор легко определить, используя стандартную функцию next(tbl, idx) :

Function pairs(tbl) return next, tbl, nil end

Функция next(tbl, idx) возвращает следующее за idx значение индекса при некотором обходе таблицы tbl (вызов next(tbl, nil) возвращает начальное значение индекса; после исчерпания элементов таблицы возвращается nil).

Фабрика ipairs(tbl) возвращает итератор, работающий совершенно аналогично описанному выше, но предназначенный для обхода таблиц, проиндексированные целыми числами начиная с 1 .

Мета-таблицы

Каждая таблица и объект типа userdata могут иметь мета-таблицу - обычную таблицу, поля которой определяют поведение исходного объекта при применении к нему некоторых специальных операций. Например, когда объект оказывается операндом при сложении, интерпретатор ищет в мета-таблице поле с именем __add и, если такое поле присутствует, то использует его значение как функцию, выполняющую сложение. Мета-таблицы позволяют определить поведение объекта при арифметических операциях, сравнениях, конкатенации и индексировании. Также можно определить функцию, вызываемую при освобождении объекта типа userdata . Индексы (имена полей) в мета-таблице называются событиями , а соответствующие значения (обработчики событий) - метаметодами .

По умолчанию вновь созданная таблица не имеет мета-таблицы. Любую таблицу mt можно сделать мета-таблицей таблицы t , вызвав функцию setmetatable(t, mt) . Функция getmetatable(t) возвращает мета-таблицу таблицы t или nil , если таблица не имеет мета-таблицы. Любая таблица может выполнять роль мета-таблицы для любой другой таблицы, в том числе и для себя.

Lua определяет следующие события:

Add, __sub, __mul, __div арифметические операции __pow возведение в степень __unm унарный минус __concat конкатенация __eq, __lt, __le операции сравнения __index доступ по отсутствующему индексу __newindex присвоение новому элементу таблицы __call вызов функции __tostring преобразование в строку __metatable получения мета-таблицы

Выражение a ~= b вычисляется как not (a == b) . Выражение a > b вычисляется как b < a . Выражение a >= b вычисляется как b <= a . При отсутствии метаметода __le операция <= вычисляется как not (b < a) т.е. с помощью метаметода __lt .

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

Обработчик события __index может быть функцией или таблицей. В случае функции обработчик вызывается и ему передается таблица и значение индекса. Такая функция должна возвращать результат индексирования. В случае таблицы происходит повторное индексирование этой таблицы тем же индексом. Если присутствует обработчик события __newindex , то он вызывается вместо присвоения значения новому элементу таблицы. Если этот обработчик является таблицей, то присвоение производится в этой таблице.

Метаметод __tostring позволяет обработать преобразование объекта (таблицы или userdata) в строку. Метаметод __metatable позволяет обработать операцию получения мета-таблицы. Если у этого поля в мета-таблице установлено значение, то функция getmetatable() будет возвращать значение этого поля, а функция setmetatable() будет завершаться с ошибкой.

Примеры использования мета-таблиц

Значение по умолчанию для полей таблиц

В следующем примере мета-таблицы используются для назначения значения по умолчанию для отсутствующих элементов таблицы:

Function set_def(t, v) local mt = { __index = function() return v end } setmetatable(t, mt) end

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

Local key = {} local mt = { __index = function(t) return t end } function set_def(t, v) t = v setmetatable(t, mt) end

Здесь локальная (пустая) таблица key используется как заведомо уникальный индекс, по которому в исходной таблице t сохраняется умалчиваемое значение v . Все таблицы, для которых устанавливается умалчиваемое значение отсутствующих полей, разделяют общую мета-таблицу mt . Мета-метод __index этой мета-таблицы перехватывает обращения к отсутствующим полям таблицы и возвращает значение, сохраненное в самой таблице по индексу key .

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

Таблица-прокси

В следующем примере пустая таблица выполняет роль прокси, переадресующего обращения к полям таблицы:

Local key = {} local mt = { __index = function(t,k) return t[k] end, __newindex = function(t,k,v) t[k] = v end } function proxy(t) local proxy = {} proxy = t setmetatable(proxy, mt) return proxy end

Здесь локальная (пустая) таблица key используется как заведомо уникальный индекс, по которому в таблице-прокси сохраняется ссылка на исходную таблицу t . Прокси представляет собой таблицу, единственный элемент которой имеет индекс key , поэтому обращение к любому элементу прокси приведет к вызову мета-метода. Общая для всех прокси мета-таблица определяет мета-методы __index и __newindex , извлекающие исходную таблицу из единственного элемента прокси, индексируя его таблицей key .

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

«Слабые» таблицы

Если объект был использован как индекс таблицы или ссылка на него была сохранена в таблице, то сборщик мусора не сможет утилизировать такой объект. В то же время, в некоторых случаях желательно иметь таблицу, в которой связь ее элементов с ключами и/или значениями «слабая» т.е. не препятствующая сбору мусора. Обычно такая необходимость возникает при кэшировании результатов вычислений в таблице и сохранении атрибутов объектов в таблице, проиндексированной самими объектами. В первом случае желательна слабая связь для значений, во втором - для индексов.

Связь элементов таблицы с объектами (значениями и индексами) определяется значением строкового поля __mode ее мета-таблицы. Если это поле содержит символ "k" , то слабой делается связь для индексов (ключей); если оно содержит символ "v" , то слабой делается связь для значений. Поле может содержать оба символа, что сделает слабой связь как для индексов, так и для значений.

Если использовать слабые таблицы, то для рассмотренной выше задачи сопоставления таблице умалчиваемого значения для отсутствующих полей можно привести следующее решение:

Local defaults = {} setmetatable(defaults, { __mode = "k" }) local mt = { __index = function(t) return defaults[t] end } function set_def(t, d) defaults[t] = d setmetatable(t, mt) end

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

Local metas = {} setmetatable(metas, { __mode = "v" }) function set_def(t, d) local mt = metas[d] if mt == nil then mt = { __index = function () return d end } metas[d] = mt end setmetatable(t, mt) end

Глобальный контекст

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

Глобальный контекст делает возможным доступ к глобальным переменным по динамически генерируемому имени:

Val = _G _G = val

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

Local f = function (t,i) return os.getenv(i) end setmetatable(_G, {__index=f})

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

Пакеты

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

My_package = {} function my_package.foo() ... end

Также можно делать все функции локальными и отдельно формировать таблицу экспортируемых функций:

Local function foo() ... end local function bar() ... end my_package = { foo = foo, bar = bar, }

Пакет загружается с помощью функции require() , причем во время загрузки имя, переданное этой функции (оно может не содержать расширения, которое добавляется автоматически) доступно через переменную _REQUIREDNAME:

If _REQUIREDNAME == nil then run_some_internal_tests() end

Классы и объекты

Конструкция tbl:func() (при объявлении функции и при ее вызове) предоставляет основные возможности, позволяющие работать с таблицей как с объектом. Основная проблема состоит в порождении многих объектов, имеющих сходное поведение т.е. порожденных от одного класса:

Function class() cl = {} cl.__index = cl -- cl будет использоваться как мета-таблица return cl end function object(cl, obj) obj = obj or {} -- возможно уже есть заполненные поля setmetatable(obj, cl) return obj end

Здесь функция class создает пустую таблицу, подготовленную к тому, чтобы стать мета-таблицей объекта. Методы класса делаются полями этой таблицы т.е. класс является таблицей, одновременно содержащей методы объекта и его мета-методы. Функция object() создает объект заданного класса - таблицу, у которой в качестве мета-таблицы установлен заданный класс. Во втором аргументе может быть передана таблица, содержащая проинициализированные поля объекта.

Some_Class = class() function Some_Class:foo() ... end function Some_Class:new() return object(self, { xxx = 12 }) end x = Some_Class:new() x:foo()

Наследование

В описанной реализации мета-таблица класса остается не использованной, что делает простой задачу реализации наследования. Класс-наследник создается как объект класса, после чего в нем устанавливающая поле __index таким образом, чтобы его можно было использовать как мета-таблицу:

Function subclass(pcl) cl = pcl:new() -- создаем экземпляр cl.__index = cl -- и делаем его классом return cl end

Теперь в полученный класс-наследник можно добавить новые поля и методы:

Der_Class = subclass(Some_Class) function Der_Class:new() local obj = object(self, Some_Class:new()) obj.yyy = 13 -- добавляем новые поля return obj end function Der_Class:bar() ... end -- и новые методы y = Der_Class:new() y:foo() y:bar()

Единственный нетривиальный момент здесь состоит в использовании функции new() из класса-предка с последующей заменой мета-таблицы посредством вызова функции object() .

При обращении к методам объекта класса-наследника в первую очередь происходит их поиск в мета-таблице т.е. в самом классе-наследнике. Если метод был унаследован, то этот поиск окажется неудачным и произойдет обращение к мета-таблице класса-наследника т.е. к классу-предку.

Основной недостаток приведенного общего решения состоит в невозможности передачи параметров в функцию new() класса-предка.

Аргументы командной строки

Переданные при запуске аргументы командной строки доступны как элементы массива arg .

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

Lua? Что это?

Lua — простой встраиваемый язык (его можно интегрировать с вашими программами, написанными на других языках), легкий и понятный, с одним типом данных, с однообразным синтаксисом. Идеальный язык для изучения.

Зачем?

Lua может вам пригодится:

* если вы геймер (плагины для World of Warcraft и множества других игр)
* если вы пишете игры (очень часто в играх движок пишут на C/C++, а AI — на Lua)
* если вы системный программист (на Lua можно писать плагины для nmap, wireshark, nginx и других утилит)
* если вы embedded-разработчик (Lua очень быстрый, компактный и требует очень мало ресурсов)

1. Научитесь программировать. Хотя бы немного. Не важно на каком языке.
2. Установите Lua. Для этого либо скачайте здесь версию 5.2 (http://www.lua.org/download.html), либо ищите ее в репозиториях. Версия 5.1 тоже пойдет, но знайте, что она очень старая.

Все примеры из статьи запускайте в терминале командой наподобие «lua file.lua».

Первые впечатления

Lua — язык с динамической типизацией (переменные получают типы «на лету» в зависимости от присвоенных значений). Писать на нем можно как в императивном, так и в объектно-ориентированном или функциональном стиле (даже если вы не знаете как это — ничего страшного, продолжайте читать). Вот Hello world на Lua:

My first lua app: hello.lua print "hello world"; print("goodbye world")

Что уже можно сказать о языке:

* однострочные комментарии начинаются с двух дефисов "--"
* скобки и точки-с-запятыми можно не писать

Операторы языка

Набор условных операторов и циклов довольно типичен:

Условные операторы (ветки else может не быть) if a == 0 then print("a is zero") else print("a is not zero") end -- сокращенная форма if/elseif/end (вместо switch/case) if a == 0 then print("zero") elseif a == 1 then print("one") elseif a == 2 then print("two") else print("other") end -- цикл со счетчиком for i = 1, 10 do print(i) end -- цикл с предусловием b = 5 while b > 0 do b = b - 1 end -- цикл с постусловием repeat b = b + 1 until b >= 5

ПОДУМАЙТЕ: что может означать цикл "for i = 1, 10, 2 do ... end" ?

В выражениях можно использовать такие вот операторы над переменными:

* присваивание: x = 0
* арифметические: +, -, *, /, % (остаток от деления), ^ (возведение в степень)
* логические: and, or, not
* сравнение: >, <, ==, <=, >=, ~= (не-равно, да-да, вместо привычного «!=»)
* конкатенация строк (оператор «..»), напр.: s1=»hello»; s2=»world»; s3=s1..s2
* длина/размер (оператор #): s=»hello»; a = #s (‘a’ будет равно 5).
* получение элемента по индексу, напр.: s

Битовых операций в языке долгое время не было, но в версии 5.2 появилась библиотека bit32, которая их реализует (как функции, не как операторы).

Типы данных

Я вам соврал, когда сказал что у языка один тип данных. Их у него много (как и у каждого серьезного языка):

* nil (ровным счетом ничего)
* булевы числа (true/false)
* числа (numbers) — без деления на целые/вещественные. Просто числа.
* строки — кстати, они очень похожи на строки в паскале
* функции — да, переменная может быть типа «функция»
* поток (thread)
* произвольные данные (userdata)
* таблица (table)

Если с первыми типами все понятно, то что же такое userdata? Вспомним о том, что Lua — язык встраиваемый, и обычно тесно работает с компонентами программ, написанными на других языках. Так вот, эти «чужие» компоненты могут создавать данные под свои нужды и хранить эти данные вместе с lua-объектами. Так вот, userdata — и есть подводная часть айсберга, которая с точки зрения языка lua не нужна, но и просто не обращать внимания на нее мы не можем.

А теперь самое важное в языке — таблицы.

Таблицы

Я вам снова соврал, когда сказал, что у языка 8 типов данных. Можете считать что он один: всё — это таблицы (это, кстати, тоже неправда). Таблица — это очень изящная структура данных, она сочетает в себе свойства массива, хэш-таблицы («ключ»-«значение»), структуры, объекта.

Итак, вот пример таблицы как массива: a = {1, 2, 3} -- массив из 3-х элементов print(a) -- выведет "2", потому что индесы считаются с единицы -- А таблица в виде разреженного массива (у которого есть не все элементы) a = {} -- пустая таблица a = 1 a = 5

ПОДУМАЙТЕ: чему равно a в случае разреженного массива?

В примере выше таблица ведет себя как массив, но на самом деле — у нас ведь есть ключи (индексы) и значения (элементы массива). И при этом ключами могут быть какие угодно типы, не только числа:

A = {} a["hello"] = true a["world"] = false a = 1 -- или так: a = { hello = 123, world = 456 } print(a["hello")) print(a.hello) -- то же самое, что и a["hello"], хотя выглядит как структура с полями

Кстати, раз уж у таблицы есть ключи и значения, то можно в цикле перебрать все ключи и соответствующие им значения:

T = { a = 3, b = 4 } for key, value in pairs(t) do print(key, value) -- выведет "a 3", потом "b 4" end

А как же объекты? О них мы узнаем чуть позже, вначале — о функциях.

Функции

Вот пример обычной функции.

Function add(a, b) return a + b end print(add(5, 3)) -- напечатает "8"

Функции языка позволяют принимать несколько аргументов, и возвращать несколько аргументов. Так аргументы, значения которых не указаны явно, считаются равными nil.

ПОДУМАЙТЕ: зачем может понадобиться возвращать несколько аргументов?

Function swap(a, b) return b, a end x, y = swap(x, y) -- кстати, это можно сделать и без функции: x, y = y, x -- и если уж функция возвращает несколько аргументов, -- а они вам не нужны - игнорируйте их с помощью -- специальной переменной-подчеркивания "_" a, _, _, d = some_function()

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

В прототипе переменное число аргументов записывается как троеточие function sum(...) s = 0 for _, n in pairs(arg) do -- в функции обращаются к ним, как к таблице "arg" s = s + n end return a end sum(1, 2, 3) -- вернет 6 sum(1, 2, 3, 4) -- вернет 10

Поскольку функции — это полноценный тип данных, то можно создавать переменные-функции, а можно передавать функции как аргументы других функций

A = function(x) return x * 2 end -- функция, умножающая на 2 b = function(x) return x + 1 end -- функция, увеличивающая на 1 function apply(table, f) result = {} for k, v in pairs(table) do result[k] = f(v) -- заменяем элемент на какую-то функцию от этого элемента end end -- ПОДУМАЙТЕ: что вернут вызовы t = {1, 3, 5} apply(t, a) apply(t, b)

Объекты = функции + таблицы

Раз мы можем сохранять функции в переменных, то и в полях таблиц тоже сможем. А это уже получаются как-бы-методы. Для тех, кто не знаком с ООП скажу, что основная его польза (по крайней мере в Lua) в том, что функции и данные, с которыми они работают находятся рядом — в пределах одного объекта. Для тех, кто знаком с ООП скажу, что классов здесь нет, а наследование прототипное.

Перейдем к примерам. Есть у нас объект, скажем, лампочка. Она умеет гореть и не гореть. Ну а действия с ней можно сделать два — включить и выключить:

Lamp = { on = false } function turn_on(l) l.on = true end function turn_off(l) l.on = false end -- это просто функции для работы со структурой turn_on(lamp) turn_off(lamp)

А если лампочку сделать объектом, и функции turn_off и turn_on сделать полями объекта, то получится:

Lamp = { on = false turn_on = function(l) l.on = true end turn_off = function(l) l.on = false end } lamp.turn_on(lamp) lamp.turn_off(lamp)

Мы вынуждены передавать сам объект лампочки в качестве первого аргумента, потому что иначе наша функция не узнает с какой именно лампочкой надо работать, чтобы сменить состояние on/off. Но чтобы не быть многословными, в Lua есть сокращенная запись, которую обычно и используют — lamp:turn_on(). Итого, мы уже знаем несколько таких упрощений синтаксиса:

Lamp:turn_on() -- самая общепринятая запись lamp.turn_on(lamp) -- то с точки зрения синтаксиса это тоже правильно lamp["turn_on"](lamp) -- и это

Продолжая говорить о сокращениях, функции можно описывать не только явно, как поля структуры, но и в более удобной форме:

Lamp = { on = false } -- через точку, тогда аргумент надо указывать function lamp.turn_on(l) l.on = true end -- через двоеточкие, тогда аргумент неявно задается сам, как переменная "self" -- "self" - и есть та лампочка, для которой вызвали метод function lamp:turn_off() self.on = false end

Интересно?

Специальные функции

Некоторые имена функций таблиц (методов) зарезервированы, и они несут особый смысл:

* __add(a, b), __sub(a, b), __div(a, b), __mul(a, b), __mod(a, b), __pow(a, b) — вызываются, когда выполняются арифметические операции над таблицей
* __unm(a) — унарная операция «минус» (когда пишут что-то типа «x = -x»)
* __lt(a, b), __le(a, b), __eq(a, b) — вычисляют результат сравнения (<, <=, ==)
* __len(a) — вызывается, когда делается "#a"
* __concat(a, b) — вызывается при "a..b"
* __call(a, …) — вызывается при "a()". Переменные аргументы — это аргументы при вызове
* __index(a, i) — обращение к a[i], при условии, что такого элемента не существует
* __newindex(a, i, v) — создание "a[i] = v"
* __gc(a) — когда объект удаляется при сборке мусора

Подменяя эти методы, можно перегружать операторы и использовать синтаксис языка для своих целей. Главное не переусердствовать.

Наследование

Для тех, кто не знает ООП, наследование позволяет расширить функциональность уже существующего класса. Например, просто лампочка умеет включаться-выключаться, а супер-ламкочка будет еще и яркость менять. Зачем нам переписывать методы turn_on/turn_off, если можно их повторно использовать?

В Lua для этого есть понятие мета-таблицы, т.е. таблицы-предка. У каждой таблицы есть одна таблица-предок, и дочерняя таблица умеет делать все, что умеет предок.

Допустим, что объект-таблицу lamp мы уже создали. Тогда супер-лампочка будет выглядеть так:

Superlamp = { brightness = 100 } -- указываем родительскую таблицу setmetatable(superlamp, lamp) -- и ее методы теперь доступны superlamp:turn_on() superlamp:turn_off()

Расширение функциональности

Родительские таблицы есть у многих типов (ну у строк и таблиц точно, у чисел и булевых чисел, и у nil их нет). Допустим, мы хотим складывать все строки с помощью оператора "+" , а не ".." . Для этого надо подменить функцию «+» (__add) для родительской таблицы всех строк:

S = getmetatable("") -- получили родительскую таблицу строки s.__add = function(s1, s2) return s1..s2 end -- подменили метод -- проверяем a = "hello" b = "world" print(a + b) -- напишет "helloworld"

Собственно, мы еще можем заменить функцию print с помощью «print = myfunction», да и много других хакерских дел можно сделать.

Области видимости

Переменные бывают глобальные и локальные. При создании все переменные в Lua являются глобальными.

ПОДУМАЙТЕ: почему?

Для указания локальной области видимости пишут ключевое слово local:

Local x local var1, var2 = 5, 3

Не забывайте об этом слове.

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

Часто, если возникают ошибки, надо прекратить выполнение определенной функции. Можно, конечно, сделать множество проверок и вызывать «return», если что-то пошло не так. Но это увеличит объем кода. В Lua используется что-то наподобие исключений (exceptions).

Ошибки порождаются с помощью функции error(x). В качестве аргумента можно передать все, что угодно (то, что имеет отношение к ошибке — строковое описание, числовой код, объект, с которым произошла ошибка и т.д.)

Обычно после этой функции вся программа аварийно завершается. А это надо далеко не всегда. Если вы вызываете функцию, которая может создать ошибку (или ее дочерние функции могут создать ошибку), то вызывайте ее безопасно, с помощью pcall():

Function f(x, y) ... if ... then error("failed to do somthing") end ... end status, err = pcall(f, x, y) -- f:функция, x-y: ее аргументы if not status then -- обработать ошибку err. В нашем случае в err находится текст ошибки end

Стандартные библиотеки

Нестандартных библиотек много, их можно найти на LuaForge, LuaRocks и в других репозиториях.

Между Lua и не-Lua

А если нам недостаточно функциональности стандартных библиотек? Если у нас есть наша программа на C, а мы хотим вызывать ее функции из Lua? Для этого есть очень простой механизм.

Допустим, мы хотим создать свою функцию, которая возвращает случайное число (в Lua есть math.random(), но мы хотим поучиться). Нам придется написать вот такой код на C:

#include #include #include /* собственно, что делать при вызове `rand(from, to)` */ static int librand_rand(lua_State *L) { int from, to; int x; from = lua_tonumber(L, 1); /* первый параметр функции */ to = lua_tonumber(L, 2); /* второй параметр функции */ x = rand() % (to - from + 1) + from; lua_pushnumber(L, x); /* возвращаемое значение */ return 1; /* возвращаем только один аргумент */ } /* в Lua "rand" соответствует нашей функции librand_rand() */ static const luaL_reg R = { {"rand", librand_rand}, {NULL, NULL} /* конец списка экспортируемых функций */ }; /* вызывается при загрузке библиотеку */ LUALIB_API int luaopen_librand(lua_State *L) { luaL_openlib(L, "librand", R, 0); srand(time(NULL)); return 1; /* завершаемся успешно */ }

Т.е. Lua предоставляет нам функции для работы с типами данных, для получения аргументов функций и возврата результатов. Функций очень мало, и они довольно простые. Теперь мы собираем нашу библиотеку как динамическую, и можем использовать функцию rand():

Random = require("librand") -- загружаем библиотеку print(random.rand(1, 100)) print(random.rand(0, 1))

А если мы хотим вызывать код, написанный на Lua из наших программ? Тогда наши программы должны создавать виртуальную машину Lua, в которой и будут выполняться Lua-скрипты. Это намного проще:

#include "lua.h" #include "lauxlib.h" int main() { lua_State *L = lua_open(); // создаем виртуальную машину Lua luaL_openlibs(L); // загружаем стандартные библиотеку luaL_dofile(L, "rand.lua"); // выполняем скрипт lua_close(L); // закрываем Lua return 0; }

Все.

Вы теперь можете писать на Lua. Если вы узнаете интересные моменты про Lua, которые можно было бы отразить в статье — пишите!

Наш сегодняшний гость - настоящий боец скрытого фронта. Вы могли видеть его в играх (World of Warcraft, Angry Birds, X-Plane, S.T.A.L.K.E.R.) или продуктах компании Adobe (Lightroom), но даже не задумывались о его существовании. Между тем этому языку уже почти 25 лет и всё это время он незаметно делал нашу виртуальную жизнь чуть лучше.

Краткая справка

Lua бы придуман в 1993 году в Католическом университете Рио-де-Жанейро. Название переводится с португальского, как Луна, причем создатели убедительно просят не писать LUA, чтобы, не дай Бог, кто-нибудь не принял название за аббревиатуру. Является мультипарадигмальным скриптовым языком, использующим прототипную модель ООП.

Типизация здесь динамическая, а для реализации наследования используются метатаблицы, то есть это прекрасный инструмент для расширений возможностей вашего продукта. Причем из-за своей компактности он пригоден для использования практически на любой платформе. Посудите сами: tarball Lua 5.3.4 весит всего 296 килобайт (в “разжатом” виде - 1.1 мегабайт), интерпретатор (написанный на C) для Linux - от 182 до 246 килобайт, а стандартный набор библиотек - ещё 421 килобайт.

Код

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

Начнем с традиционного:

print("Hello World")

Согласитесь, знакомо и не слишком информативно. Более интересный пример с точки зрения знакомства с Lua - вычисление факториала введенного числа:

Function fact (n)
if n == 0 then
return 1
else
return n * fact(n-1)
end
end

Print("enter a number:")
a = io.read("*number") -- read a number
print(fact(a))

Все предельно понятно. Кстати, в Lua поддерживается параллельное присваивание:

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

#include
#include
#include
#include
#include

Int main (void) {
char buff;
int error;
lua_State *L = lua_open(); /* opens Lua */
luaopen_base(L); /* opens the basic library */
luaopen_table(L); /* opens the table library */
luaopen_io(L); /* opens the I/O library */
luaopen_string(L); /* opens the string lib. */
luaopen_math(L); /* opens the math lib. */

While (fgets(buff, sizeof(buff), stdin) != NULL) {
error = luaL_loadbuffer(L, buff, strlen(buff), "line") ||
lua_pcall(L, 0, 0, 0);
if (error) {
fprintf(stderr, "%s", lua_tostring(L, -1));
lua_pop(L, 1); /* pop error message from the stack */
}
}

Lua_close(L);
return 0;
}

Преимущества и недостатки

Итак, чем же хорош Lua?

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

Среды разработки

LDT (Lua Development Tools) для Eclipse - расширение для одной из наиболее популярных IDE;

ZeroBrane Studio - специализированная среда, написанная на Lua;

Decoda - не самая популярная кроссплатформенная IDE, но в качестве альтернативы подойдет;

SciTE - хороший редактор, полноценно поддерживающий Lua;

WoWUIDesigner - угадайте, для какой игры эта среда помогает обрабатывать скрипты, в том числе на Lua?

Полезные ссылки

http://www.lua.org/home.html - официальный сайт со всей необходимой информацией, учебником, книгами, документацией и даже есть немного специфического юмора;

http://tylerneylon.com/a/learn-lua/ - отличная обучалка от Tyler Neylon. Подойдет программистам с опытом, кто хорошо знает английский язык (впрочем, со словарем тоже не возникнет больших проблем) и просто желает расширить свой кругозор;

https://zserge.wordpress.com/2012/02/23/lua-за-60-минут/ - основы Lua за 60 минут от явно неравнодушного к этому языку программиста. На русском языке;

http://lua-users.org/wiki/LuaTutorial - вики-учебник;

https://youtube.com/watch?v=yI41OL0-DWM - видеоуроки на YouTube, которые помогут вам наглядно разобраться с настройкой IDE и базовыми принципами языка.

Скрипты на языке Lua

Написанный на Lua скрипт не имеет какой-либо специальной функции, с которой начиналось бы его выполнение. Скрипт можно рассматривать просто как набор команд (инструкций), который выполняется, начиная с первой инструкции.

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

Работа с переменными в Lua

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

Имена переменных в Lua

Именами (идентификаторами) переменных в Lua могут быть любые последовательности из букв, цифр и символа подчеркивания, начинающиеся не с цифры.

Обратите внимание

Язык Lua различает регистр символов, поэтому abc, Abc, ABC являются различными именами.

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

and break do else elseif

end false for function if

in local nil not or

repeat return then true until

Кроме того, все имена, начинающиеся с символа подчеркивания, за которым идут заглавные буквы (например, _VERSION) также являются зарезервированными.

Какие переменные бывают в Lua?

Переменные в Lua могут быть глобальными и локальными. Если переменная не объявлена явно как локальная, она считается глобальной.

Глобальные переменные Lua

Глобальная переменная появляется в момент присваивания ей первого значения. До присваивания первого значения обращение к глобальной переменной даёт nil.

MsgBox(tostring (g)) --> nil

MsgBox(tostring (g)) --> 1

Глобальная переменная существует до тех пор, пока существует среда исполнения скрипта и доступна любому Lua-коду, выполняемому в этой среде.

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

g = 1 - создаем глобальную переменную g со значением 1

g = nil - удаляем глобальную переменную g

MsgBox(tostring (g)) --> nil

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

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

Любые локальные переменные должны быть объявлены явно с использованием ключевого слова local. Объявить локальную переменную можно в любом месте скрипта. Объявление может включать в себя присваивание переменной начального значения. Если значение не присвоено, переменная содержит nil.

local a - объявляем локальную переменную a

local b = 1 - объявляем локальную переменную b, присваиваем ей значение 1

local c, d = 2, 3 - объявляем локальные переменные c и d, присваиваем им значения 2 и 3

Область видимости локальной переменной начинается после объявления и продолжается до конца блока.

Примечание

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

Под блоком понимается:

тело управляющей конструкции (if-then, else, for, while, repeat);

тело функции;

фрагмент кода, заключённый в ключевые слова do...end.

Если локальная переменная определена вне какого-либо блока, её область видимости распространяется до конца скрипта.

local i = 1 - переменная i локальна в пределах скрипта

while i <= a do - цикл от 1 до 5

local a = i^2 - переменная а локальна внутри цикла while

MsgBox(a) --> 1, 4, 9, 16, 25

MsgBox(a) -->

if i > 5 then

local a - переменная а локальна внутри then

MsgBox(a) --> 10

MsgBox(a) --> 5 (здесь обращение к глобальной a)

local a = 20 - переменная а локальна внутри do-end

MsgBox(a) --> 20

MsgBox(a) --> 5 (здесь обращение к глобальной a)

Обратите внимание

Когда возможно, рекомендуется использовать локальные переменные вместо глобальных. Это позволит избежать «засорения» глобального пространства имён и обеспечит лучшую производительность (поскольку доступ к локальным переменным в Lua выполняется несколько быстрее, чем к глобальным).

Типы данных Lua

Какие типы данных поддерживает язык Lua?

Lua поддерживает следующие типы данных:

1. Nil (ничего). Соответствует отсутствию у переменной значения. Этот тип представлен единственным значением - nil.

2. Boolean (логический). К данному типу относятся значения false (ложь) и true (истина).

При выполнении логических операций значение nil рассматривается как false. Все остальные значения, включая число 0 и пустую строку, рассматриваются как true.

3. Number (числовой). Служит для представления числовых значений.

В числовых константах можно указывать необязательную дробную часть и необязательный десятичный порядок, задаваемый символами «e» или «E». Целочисленные числовые константы можно задавать в шестнадцатеричной системе, используя префикс 0x.

Примеры допустимых числовых констант: 3, 3.0, 3.1415926, 314.16e-2, 0xff.

4. String (строковый). Служит для представления строк.

Строковые значения задаются в виде последовательности символов, заключённой в одинарные или двойные кавычки:

a = «это строка»

b = "это вторая строка"

Строки, заключённые в двойные кавычки, могут интерпретировать C-подобные управляющие последовательности (escape-последовательности), начинающиеся с символа «\» (обратный слэш):

\b (пробел),

\n (перевод строки),

\r (возврат каретки);

\t (горизонтальная табуляция),

\\ (обратный слеш);

\"" (двойная кавычка);

\" (одинарная кавычка).

Обратите внимание

Символ в строке также может быть представлен своим кодом с помощью escape-последовательности:

где ddd - последовательность из не более чем трёх цифр.

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

Определение строки с помощью двойных квадратных скобок позволяет игнорировать все escape-последовательности, т. е. строка создаётся полностью так, как описана:

local a = [] в Lua]=]

Будет срока: «определение строки [] в Lua»

5. Function (функция). Функции в Lua могут быть записаны в переменные, переданы как параметры в другие функции ивозвращены как результат выполнения функций.

6. Table (таблица). Таблица представляет собой набор пар «ключ» - «значение», которые называют полями илиэлементами таблицы. Как ключи, так и значения полей таблицы могут иметь любой тип, за исключением nil. Таблицы не имеют фиксированного размера: в любой момент времени в них можно добавить произвольное число элементов.

Подробнее - в статье «Создание таблиц в Lua»

7. Userdata (пользовательские данные). Является особым типом данных. Значения этого типа не могут быть созданы или изменены непосредственно в Lua-скрипте.

Userdata используется для представления новых типов, созданных в вызывающей скрипт программе или в библиотеках, написанных на языке С. Например, библиотеки расширений Lua для «CronosPRO» используют этот тип для представления таких объектов, как:

банки данных (класс Bank);

базы данных (класс Base);

записи (класс Record) и т. п.

8. Thread (поток). Соответствует потоку выполнения. Эти потоки никаким образом не связаны с операционной системой и поддерживаются исключительно средствами самого Lua.

Как в Lua задать тип переменной?

Lua не предусматривает явного задания типа переменной. Тип переменной устанавливается в момент присвоения переменной значения. Любой переменной может быть присвоено значение любого типа (вне зависимости от того, значение какого типа она содержала ранее).

a = 123 - переменная a имеет тип number

a = «123» - теперь переменная a имеет тип string

a = true - теперь переменная a имеет тип boolean

a = {} - теперь переменная a имеет тип table

Обратите внимание

Переменные типа table, function, thread и userdata не содержат самих данных, а хранят ссылки на соответствующие объекты. При присваивании, передачи в функцию в качестве аргумента и возвращении из функции в качестве результата копирования объектов не происходит, копируются только ссылки на них.

a = {} - создаем таблицу. В переменную a помещается ссылка на таблицу

b = a - переменная b ссылается на ту же таблицу, что и a

a = 10 - элементу таблицы с индексом 1 присвоено значение 10

MsgBox(b) --> "10"

MsgBox(a) --> "20"

Остальные данные являются непосредственными значениями.

MsgBox(a) --> "20"

MsgBox(b) --> "10"

Как в Lua получить тип переменной?

Тип значения, сохранённого в переменной, можно выяснить при помощи стандартной функции type. Эта функция возвращает строку, содержащую название типа («nil», «number», «string», «boolean», «table», «function», «thread», «userdata»).

t = type («это строка») - t равно «string»

t = type (123) - t равно «number»

t = type (type) - t равно «function»

t = type (true) - t равно «boolean»

t = type (nil) - t равно «nil»

t = type (CroApp.GetBank()) - t равно «userdata»

Как в Lua преобразовать тип переменной?

Lua при необходимости автоматически преобразует числа в строки и наоборот. Например, если строковое значение является операндом в арифметической операции, оно преобразуется в число. Аналогично числовое значение, встретившееся в том месте, где ожидается строковое, будет преобразовано в строку.

a = «10» + 2 - a равно 12

a = «10» + 2 - a равно «10 + 2»

a = "-5.3e-10"*«2» - a равно -1.06e-09

a = «строка» + 2 - Ошибка! Невозможно преобразовать «строка» в число

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

a = tostring (10) - a равно «10»

a = tostring (true) - a равно «true»

a = tostring (nil) - a равно «nil»

a = tostring ({ = «это поле 1»}) - a равно «table: 06DB1058»

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

a = render (10) - a равно «10»

a = render (true) - a равно «true»

a = render (nil) - a равно «nil»

a = render ({ = «это поле 1»}) - a равно "{ = «это поле 1»}"

Для явного преобразования значения в число можно использовать стандартную функцию tonumber. Если значение является строкой, которую можно преобразовать в число (или уже является числом), функция возвращает результат преобразования, в противном случае возвращает nil.

a = tonumber («10») - a равно «10»

a = tonumber («10»..".5") - a равно 10.5

a = tonumber (true) - a равно «nil»

a = tonumber (nil) - a равно «nil»

Расстановка комментариев в Lua

Комментарий в Lua начинается двумя знаками «минус» (--) и продолжается до конца строки.

local a = 1 - однострочный комментарий

Если непосредственно после символов «--» идут две открывающие квадратные скобки ([[), комментарий являетсямногострочным и продолжается до двух закрывающих квадратных скобок (]]).

local a = 1 - [[ многострочный

комментарий ]]

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

local a = [[Компания «Кронос»]] - [=[

local a = [[Компания «Кронос»]]

Количество символов «=» определяет вложенность:

local a = [=[определение некоторой строки [] в языке Lua]=] --[==[

local a = [=[определение некоторой строки [] в языке Lua]=]

Операции, применяемые в Lua

В выражениях, написанных на Lua, могут применяться следующие виды операций:

1. Арифметические операции.

Lua поддерживает следующие арифметические операции:

+ (сложение);

- (вычитание);

* (умножение);

/ (деление);

^ (возведение в степень);

% (остаток от деления).

Обратите внимание

Арифметические операции применимы как к числам, так и к строкам, которые в этом случае преобразуются в числа.

2. Операции сравнения.

В Lua допустимы следующие операции сравнения величин:

== (равно);

~= (не равно);

< (меньше);

> (больше);

<= (меньше или равно);

>= (больше или равно).

Обратите внимание

Операции сравнения всегда возвращают логическое значение true или false.

Правила преобразования чисел в строки (и наоборот) при сравнениях не работают, т. е. выражение «0» == 0 даёт в результате false.

3. Логические операции.

К логическим операциям относятся:

and (логическое И).

Операция and возвращает свой первый операнд, если он имеет значение false или nil. В противном случае, операция возвращает второй операнд (причём этот операнд может быть произвольного типа).

a = (nil and 5) - a равно nil

a == (false and 5) - a равно false

a == (4 and 5) - a равно 5

or (логическое ИЛИ).

Операция or возвращает первый операнд, если он не false и не nil, иначе он возвращает второй операнд.

a == (4 or 5) - a равно 4

a == (false or 5) - a равно 5

Обратите внимание

Логические операции and и or могут возвращать значения любых типов.

Логические операции and и or вычисляют значение второго операнда только в том случае, если его нужно вернуть. Если этого не требуется, второй операнд не вычисляется. Например:

a == (4 or f()) - вызова функции f() не произойдет

not (логическое НЕ).

Операция not всегда возвращает true или false.

4. Операция конкатенации.

Для конкатенации (объединения) строк служит операция… (две точки).

a = «Кронос».."-"..«Информ» - переменная a получит значение «Кронос-Информ»

Обратите внимание

Если один или оба операнда являются числами, выполняется их преобразование в строки.

a = 0..1 - переменная a получит значение «01»

5. Операция получения длины.

В Lua определена операция длины #, которую можно использовать для получения длины строки.

a = «строка»

len = #a - len равно 6

len = #«ещё строка» - len равно 10

Обратите внимание

С помощью операции # можно также узнать максимальный индекс (или размер) массива. Подробнее - в статье «Работа с массивами в Lua» .

Приоритет операций в Lua

В языке Lua выполнение операций осуществляется в соответствии со следующим приоритетом (в порядке убывания):

2. not # - (унарный)

6. < > <= >= ~= ==

Вызов скриптов из форм

С каждой формой (включая вложенные формы) связан отдельный скрипт, который обычно содержит функции, выполняющие обработку событий формы и её элементов.

Когда форма запускается, её скрипт загружается в глобальное окружение. При возникновении события формы или её элемента система вызывает сопоставленную этому событию функцию-обработчик.

Необходимо отметить, что скрипт формы, хотя и не содержит вызова функции module, фактически является модулем. Это означает, что переменные, объявленные в скрипте формы без ключевого слова local, не выносятся в глобальное окружение и доступны только внутри этого скрипта. Если необходимо сделать какое-либо значение доступным для скриптов других форм, его следует явным образом определить в глобальной таблице _G:

local a = _G.var

Блоки операторов (инструкций)

К основным операторам Lua относятся:

присваивание;

условный оператор;

операторы для организации циклов.

Группа операторов может быть объединена в блок (составной оператор) при помощи конструкции do… end.

do - начало блока

<оператор1> - тело блока

<оператор2>

<операторN>

end - конец блока

Блок открывает новую область видимости, в которой можно определять локальные переменные.

a = 5 - глобальная переменная a

local a = 20 - внутри do-end определяется локальная переменная а

MsgBox(a) --> 20

MsgBox(a) --> 5 (здесь обращение уже к глобальной a)

Оператор присваивания в Lua

Присваивание изменяет значение переменной или поля таблицы. В простейшем виде присваивание может выглядеть так:

a = 1 - переменной a присвоено значение 1

a = b + c - переменной a присвоена сумма значений переменных b и с

a = f(x) - переменной a присвоено значение, возвращённое функцией f(x)

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

a, b = 1, 5*c - a равно 1; b равно 5*c

Если переменных больше чем значений, «лишним» переменным присваивается nil.

a, b, c = 1, 2 - a равно 1; b равно 2; c равно nil

Если значений больше чем переменных, «лишние» значения игнорируются.

a, b = 1, 2, 3 - a равно 1; b равно 2; значение 3 не использовано

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

a = 10; b = 20 - a равно 10, b равно 20

a, b = b, a - теперь a равно 20, b равно 10

Условный оператор (if) в Lua

Оператор if проверяет истинность заданного условия. Если условие является истинным, выполняется часть кода, следующая за ключевым словом then (секция then). В противном случае, выполняется код, следующий за ключевым словом else (секция else).

if a > b then

return a - если a больше b, вернуть a

return b - в противном случае - вернуть b

Секция else является необязательной.

if a < 0 then

a = 0 - если a меньше 0, присвоить a значение 0

Вместо вложенных операторов if можно использовать конструкцию elseif. Например, приведенный код:

будет проще для восприятия, если заменить его следующим:

return «Иван» - если a равно 1

elseif a == 2 then

return «Петр» - если a равно 2

elseif a == 3 then

return «Сергей» - если a равно 3

return «Нет такого игрока» - если a - ни одно из перечисленных

Цикл с предусловием (while) в Lua

Оператор while предназначен для организации циклов с предусловием и имеет следующий вид:

while do

… - тело цикла

Перед каждой итерацией цикла проверяется условие :

если условие ложно, цикл завершается и управление передаётся первому оператору, следующему за оператором while;

если условие истинно, выполняется тело цикла, после чего все действия повторяются.

while i > 0 do - цикл от 10 до 1

t[i] = «поле »..i

a = {3, 5, 8, -6, 5}

while i > 0 do - ищем в массиве отрицательное значение

if a[i] < 0 then break end - если найдено, прерываем цикл

i = i - 1 - иначе переходим к следующему элементу

if i > 0 then

MsgBox («Индекс отрицательного значения: »..i)

MsgBox («Массив не содержит отрицательных значений»)

Примечание

Цикл с постусловием (repeat) в Lua

Оператор repeat предназначен для организации циклов с постусловием и имеет следующий вид:

… - тело цикла

until

Тело цикла выполняется до тех пор, пока условие не станет истинным. Проверка условия осуществляется после выполнения тела цикла, поэтому в любом случае тело цикла выполнится хотя бы один раз.

Суммируем значения массива a, пока сумма не превысит 10

a = {3, 2, 5, 7, 9}

sum = sum + a[i]

until sum > 10

MsgBox («Сложено »..i.." элементов. Сумма равна "..sum)

Для выхода из цикла до его завершения можно использовать оператор break.

Примечание

Подробнее об особенностях использования оператора break - в статье «Операторы break и return»

Циклы с оператором for в Lua

Оператор for предназначен для организации циклов и допускает две формы записи:

простую (числовой for);

расширенную (универсальный for).

Простая форма оператора for

Простая форма оператора for имеет следующий вид:

for var = exp1, exp2, exp3 do

… - тело цикла

Тело цикла выполняется для каждого значения переменной цикла (счётчика) var в интервале от exp1 до exp2, с шагом exp3.

Примечание

Шаг может не задаваться. В этом случае он принимается равным 1.

for i = 1, 10 do - цикл от 1 до 10 с шагом 1

MsgBox («i равно »..i)

for i = 10, 1, -1 do - цикл от 10 до 1 с шагом -1

MsgBox («i равно »..i)

Обратите внимание

Выражения exp1, exp2 и exp3 вычисляются всего один раз, перед началом цикла. Так, в примере ниже, функция f(x) будет вызвана для вычисления верхнего предела цикла только один раз:

for i = 1, f(x) do - цикл от 1 до значения, возвращенного функцией f()

MsgBox («i равно »..i)

Переменная цикла является локальной для оператора цикла и по его окончании не определена.

for i = 1, 10 do - цикл от 1 до значения, возвращенного функцией f()

MsgBox («i равно »..i)

MsgBox («После выхода из цикла i равно »..i) - Неверно! i равно nil

Обратите внимание

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

Для выхода из цикла до его завершения используется оператор break.

a = {3, 5, 8, -6, 5}

for i = 1,#a do - ищем в массиве отрицательное значение

if a[i] < 0 then - если найдено...

index = i - сохраняем индекс найденного значения...

break - и прерываем цикл

MsgBox («Индекс отрицательного значения: »..index)

Примечание

Подробнее об особенностях использования оператора break - в статье «Операторы break и return»)

Введение

Это руководство предназначено для тех, у кого ограниченный опыт работы с LUA. Мы рассмотрим основы того, как оформлять код, строительные блоки для Вас, чтобы создавать более сложный код и предоставим некоторые примеры. Руководство написано так, чтобы сразу применять его на практике. Поэтому Вам следует открыть Tabletop Simulator и Ваш редактор LUA, чтобы следовать дальше.

Это первое руководство в этой серии. Второй – это Изучение Lua Подробнее. Третий представляет собой набор полезных функций под названием Learning Lua Functions.

Перед первым нажатием клавиши

Во-первых, я бы настоятельно рекомендовал установить Atom, если вы собираетесь делать скрипты в Tabletop Simulator. Он знает, какие функции можно использовать и будет импортировать/экспортировать код в/из TTS.

Затем Вы должны добавить в закладках . Вы будете часто ссылаться на этот сайт, как только начнете писать свои скрипты. Здесь Вы найдете специальные функции в Tabletop Simulator и как они работают. Вы чаще всего будете использовать страницы API и Object, по крайней мере, на моем опыте.

Подготовка

Когда Вы сохраняете свои скрипты в Tabletop, он будет использовать Ваше последнее сохранение, а затем загрузит в него скрипты. Поэтому для любого скрипта, который Вы намереваетесь написать, Вам нужно будет сделать следующее:

  • Подготовьте стол так, как Вы этого хотите.
  • Сохраните стол.
  • Загрузите стол.
Для этого упражнения возьмите пустой стол и создайте два объекта (я использовал квадратный блок и прямоугольный блок), а также красную шашку.

Повторно нажимайте его, потому что, конечно, вы это сделаете.

EXTRA CREDIT: Когда вы создаете таблицы, есть несколько способов сделать это . Способ, используемый здесь, заключался в том, чтобы обеспечить визуальную ясность. Однако такой способ создания параметров кнопки, как этот, занимает не мало места, если у Вас будет много кнопок. Я предпочитаю создавать свои таблицы таким образом, чтобы экономить место, но не выходить за правых край. Используя наш пример, я бы создал таблицу параметров следующим образом:

button_parameters = { click_function="buttonClicked", function_owner=nil, label="Press Me", position={0,0.8,0}, rotation={0,0,0}, width=500, height=500, font_size=100 }

EXTRA CREDIT: Это идеальный момент для начала игры с разными вещами, которые вы можете делать с объектами. Перейдите на страницу «Объект» в Knowledge Base и попробуйте материал. Двигайте объекты, заставляйте их переключаться на позиции, менять их цвета, что бы вы ни думали.

EXTRA CREDIT: Кроме того, при каждом нажатии кнопки функция click_function запускается с двумя параметрами. Первая - это ссылка на объект, в частности ссылка на объект, к которому привязана кнопка. Второй - это цвет (например, "Blue" - синий) в строчном формате цвета игрока, который нажал на кнопку.

5) Логические утверждение

Сравнение переменных

Еще раз удалите все скрипты внутри функции buttonClicked(). Мы собираемся создать новую переменную, а затем изменить ее. Новая переменная будет булевского типа. Булевские значения могут быть только true, false. Булевские значения всегда записываются маленькими буквами. Во-первых, мы создадим нашу переменную под нашим идентификатором GUID объектов и шашки.

trueOrFalse = true

Затем, в buttonClicked, мы установим некоторую логику, чтобы проверить, истинно ли значение trueOrFalse. Если оно истинно, то будет печатать, что это Истина, и переключит его на Ложь. Если кнопка снова нажата, будет печатать, что это Ложь, и переключит значение на Истина.

if trueOrFalse then print("trueOrFalse was true.") --trueOrFalse была истина. trueOrFalse = false else print("trueOrFalse was false.") --trueOrFalse была ложна. trueOrFalse = true end

Мы могли бы также написать это так "if trueOrFalse == true then", но это необязательно. Помните, что оператору IF нужно передать булевское значение. И так как trueOrFalse уже является одним из таких, мы можем отпустить "== true".

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

Это ещё один тип – ipairs. Pairs нужны для таблиц без числовых ключей, а ipairs нужны для таблицы с последовательными числовыми ключами (массивы). ipairs идет по порядку, когда pairs может идти в любом порядке.

  • Сергей Савенков

    какой то “куцый” обзор… как будто спешили куда то