В этой заключительной третей части большой обзорной статьи по языку Lua в Corona SDK будут рассмотрены очень важные вопросы, после изучения которых вы сможете перейти непосредственно к изучения Corona SDK.
- Циклические операции
- Работа с файлами
- Дата и время
- Функции
- Регулярные выражения и захваты
- Управление случайностью
- Математические функции
- Бережное отношение к чужому
Возможно кто-то не читал первые две части этой статьи им я советую начать именно с их прочтения: первая часть и вторая часть
Циклические операции
В любой языке и почти для любой задачи обязательно возникнет ситуация при которой некоторый участок кода необходимо выполнить несколько раз. Для этой задачи используются циклические операции. В языке Lua имеется 2 оператора позволяющих организовать циклы(for и while), далее я опишу их.
while (exp) do end
Код между do и end выполняется до тех пор пока результат выражения exp — истина (true). Приведу пример реализации цикла:
local count = 4--количество повторений
local b = true--флаг выхода из цикла
local i = 0--создаем и обнуляем счетчик
while b do--цикл выполняется пока b равно true
i = i + 1--увеличиваем счетчик
print(i)--печать текущего значения счетчика
if i >= count then--если счетчик достиг
b = false--ставим флаг выхода из цикла
end
end
-->> 1
-->> 2
-->> 3
-->> 4
Переменной b мы до цикла устанавливаем значение true а дальше после достижения нужного состояния счетчика ставим b равное false и происходит выход из цикла. Существует способ создания цикла в формате while true do end при этом создается вечный цикл — это достаточно опасная ситуация и всегда нужно иметь устойчивые условия выхода из таких циклов, в Lua для этих целей имеется оператор break — этот оператор безусловно завершает выполнения цикла. Рассмотрим пример:
local count = 4--количество повторений
local i = 0--создаем и обнуляем счетчик
while true do--вечный цикл
i = i + 1--увеличиваем счетчик
print(i)--печать текущего значения счетчика
if i >= count then--если счетчик достиг
break--выход из цикла
end
end
-->> 1
-->> 2
-->> 3
-->> 4
Не допускайте халатности при организации вечных циклов и в целом при использовании while. Не всегда можно точно предсказать, что в любой ситуации цикл будет завершен, а это приводит к глубокому зависанию приложения.
for (exp) do end
Код расположенный между do и end будет выполнять согласно правил установленных в exp. Цикл for имеет широкое распространение среди языком программирования, в Lua этот оператор можно применять двумя способами, ниже мы рассмотрим их. Первый и самый простой способ легко будет понятен любому пользователю знакомому с любым языком программирования:
for i = 1,3 do--цикл выполняется 3 раза в порядке увеличения
print(i)
end
-->> 1
-->> 2
-->> 3
В некоторых случаях увеличивать итератор необходимо не на единицу, а к примеру на 2 или 5, так же возможен вариант при котором вы хотите считать в обратную сторону. Это реализуется использование 3 параметра for, который определяет величину приращения за один шаг.
--считаем от 2 до 6 прибавляя по 2
for i = 2,6,2 do
print(i)
end
-->> 2
-->> 4
-->> 6
--считаем от 9 до 3 отнимая по 3
for i = 9,3,-3 do
print(i)
end
-->> 9
-->> 6
-->> 3
for [values] in (func) do end
Этот вариант цикла for уже рассматривался в прошлой части когда мы рассматривали функции string.gmath, pairs и ipairs. В Lua использование этого цикла можно считать большим удобством и не стоит его избегать, проще один раз разобраться и дальше делать многие вещи проще чем привычным способом свойственным в других языках. Еще раз приведу примеры применения этого типа циклической операции:
local reg = " (%w+):(%d+)"--регулярное выражение с захватом 2 параметров
local s = " masha:30, lena:25, olia:26, kira:29, "--строка для поиска и захвата
for key, value in s:gmatch(reg) do--цикл с захватом всех вхождений
print(key, value)
end
-->> masha 30
-->> lena 25
-->> olia 26
-->> kira 29
for key, value in ipairs{'vasia','petia','kolia'} do
print(key, value)
end
-->> 1 vasia
-->> 2 petia
-->> 3 kolia
for key, value in pairs{v1 = 'OK',v2 = 123, v3 = 12.3} do
print(key, value)
end
-->> v1 OK
-->> v2 123
-->> v3 12.3
Буквально в следующем разделе при рассмотрении работы с файлами вы мы рассмотрим еще один вариант циклической функции в качестве аргумента цикла — file:lines.
Работа с файлами
Для работы с файлами в Corona SDK используется стандартная библиотека ввода-вывода Lua — io. Не все функции библиотеки имеют кроссплатформеную реализацию, так же не все функции в Corona SDK реализованы так же как это принято в Lua. Наряду с работой с файлами библиотеку io в Lua принято использовать для работы с консолью (если не указывать идентивфикатор файла), но в Corona SDK это действие будет происходить не совсем верно, приведу пример:
io.write('A:')
local A = io.read()
io.write('B:')
local B = io.read()
io.write('A + B = ',A + B,'n')
Это фрагмент кода в Lua работает так: выполняется первая строка в консоль выводится строка «A:», далее ожидается пока пользователь введет число и нажмет Enter, далее та же операция проходит с переменной B после чего происходит вывод результата суммирования. В Corona SDK не происходит остановка выполнения на второй и четвертой строке, и как следствие возникает ошибка на 5 строке при попытке сложить 2 переменные равные nil. Т.е. создание приложений принимающих параметры из консоли в Corona SDK не возможно, т.е. для работы с консолью остается использовать только io.write для вывода текста, но у этой функции есть прямой аналог — print. Из выше сказанное стоит сделать вывод, что для работы с консолью нет смысла использовать библиотеку io. Для работы с файлами библиотека годится, но опять же из-за сомнительной реализации в Corona SDK использовать библиотеку лучше самым очевидным и простым способом: «Открыл/Создал — Прочел — Записал — Закрыл». Так же при разработке в Corona SDK приходится учитывать что особую систему каталогов свойственную мобильным приложениям, именно с этого мы и начнем.
Каталоги доступные приложению
Все данные приложения находятся в изолированном от других приложении пространстве, имеется несколько каталогов:
- Главный каталог(system.ResourceDirectory) — это каталог по умолчанию, он соответствует каталогу размещения файла main.lua. Запись в этот каталог не доступна, можно только читать.
- Каталог документов(system.DocumentsDirectory) — в этом каталоге принято хранить настройки пользователя и другие файлы которые приложение создает при установке и в дальнейшем он не должны удалять. Этот каталог доступен как для записи так и для чтения и данные в нем будут удалены только при удалении приложения. В iOS этот каталог используется в системе синхронизации данных приложений, т.е. пользователь получит свои настройки для вашего приложения на другом устройстве если синхронизирует настройки.
- Временный каталог(system.TemporaryDirectory) — в этом каталоге нужно создавать файлы требуемые в рамкой одной сессии, если приложение завершить, операционная система оставляет за собой право удалить эти файлы. Не размещайте файлы настроек в этот каталог. Доступен для записи так и для чтения.
- Дополнительный каталог(system.CachesDirectory) — этот каталог имеет более длительный цикл хранения данных, но все же для важной информации его не стоит использовать. В iOS этот каталог не используется в системе синхронизации данных приложений, т.е. если вы целенаправленно хотите избежать что бы ваши игроки не могли синхронизировать настройки между различными своими устройствами, можете хранить данные в этом каталоге. Доступен для записи так и для чтения.
При работе с файлами используется функции открытия файла они требуют указания пути к файлу, путь состоит из директории и имени файла, для формирования корректного значения пути файла в Corona SDK используется функция system.pathForFile, функцуия применяется следующим образом:
local fileName = 'saves.dat'
local filePath = system.pathForFile(fileName, system.DocumentsDirectory )
В качестве второго параметра необходимо использовать одно из доступных значений: system.ResourceDirectory, system.DocumentsDirectory, system.TemporaryDirectory, system.CachesDirectory. Как уже выше говорилось значение по умолчанию для system.ResourceDirectory, т.е. если не указать второй параметр корнем пути будет считать каталог с файлом main.lua.
Многие функции в Corona SDK использую файлы для своей работы, например display.newImage загружает изображение из файла на экран устройства в этих функциях имя файла вводится отдельно от каталога:
local mouse = display.newImage( "assets/images/mouse.png", system. ResourceDirectory, 0, 0 )
Так как system. ResourceDirectory — значение по умолчанию, этот параметр можно не вводить, сократив тем самым запись:
local mouse = display.newImage( "assets/images/mouse.png", 0, 0 )
Обратите внимание на 3 вещи:
- к имени файла можно добавлять и каталоги
- начало пути не содержит слешь "/"
- в пути используется «прямой слешь /», (как в браузере или UNIX системах), можно использовать в пути «обратный слешь » (как в Windows) но этот символ является экранирующим и если сильно хочется применить именно его используйте двойной символ "\", так как на самом деле он будет восприниматься как одинарный
ВАЖНО: Одной из самых популярных причин ошибки при которой приложение работает на симуляторе компьютера, но не работает на устройстве, является применение различного регистра в именах каталогов и файлов. Дело в том что в симуляторе нет зависимости от регистра, т.е. Assets/Images/Mouse.PNG и assets/images/mouse.png — считается одним и тем же путем, в Android и iOS это разные пути, т.о. если в функции указан путь в нижнем регистре, а реально имена каталогов или файлов имеют символы верхнего регистра(или наоборот) — это приводит к ошибке. Старайтесь везде и всегда избегать имен каталогов и файлов с символами верхнего регистра.
Далее рассмотрим наиболее простые и приемлемые приемы работы с файлами. В этом уроке я не буду писать свой вариант перевода официальных справочных материалов их вы можете сами почитать как для Lua так и для короны, я постараюсь выдать вам самое нужное, а именно как сделать удобное хранение настроек проекта и реализовать работу с логами.
Создание/Запись/Чтение файла целиком
Реализуем простой пример, в котором файл создается и в него записывается строка:
local write_data = "Hello file!"--строка для записи в файл
local fileName = 'saves.dat'--имя файла
local filePath = system.pathForFile(fileName, system.DocumentsDirectory )--путь к файлу
local file = io.open(filePath, "w")--открываем файл с его созданием для записи
file:write( write_data )--записываем
io.close( file )--закрываем файл
Рассмотрим особенности. В функции io.open в качестве второго параметра используется особый флаг, который определяет тип доступа к файлу, рассмотрим основные флаги доступа:
- «r» — режим чтения (по умолчанию); указатель файла помещается в начале файла
- «w» — режим только для записи; перезаписывает файл, если файл существует. Если файл не существует, создается новый файл для записи.
- «a» — режим добавления (только для записи); указатель файла находится в конце файла, если файл существует (файл находится в режиме добавления). Если файл не существует, он создает новый файл для записи.
- «r+» — режим обновления (чтение / запись); все предыдущие данные сохранены. Указатель файла будет в начале файла. Если файл существует, он будет перезаписан, только если вы явно напишите ему.
- «w+» — режим обновления (чтение / запись); все предыдущие данные стираются. Перезаписывает существующий файл, если файл существует. Если файл не существует, создается новый файл для чтения и записи.
- «a+» — добавить режим обновления (чтение / запись); предыдущие данные сохраняются, и запись разрешается только в конце файла. Указатель файла находится в конце файла, если файл существует (файл открывается в режиме добавления). Если файл не существует, он создает новый файл для чтения и записи.
Реализуем пример открытия файла и чтения из него всего содержимого (строки записанной в прошлый раз):
local fileName = 'saves.dat'--имя файла
local filePath = system.pathForFile(fileName, system.DocumentsDirectory )--путь к файлу
local file = io.open(filePath, "r")--открываем файл для чтения
local read_data = file:read("*a")--читаем весь файл
io.close( file )--закрываем файл
print(read_data)
ВАЖНО: Если вы хотите работая в эмуляторе просмотреть файлы созданные в каталогах проекта необходимо выполнить действие: File -> Show Project Sandbox, при этом откроется корневой каталог проекта (system.ResourceDirectory), в этой папке вы обнаружите три вложенные папки:
- CachedFiles — system.CachesDirectory
- Documents — system.DocumentsDirectory
- TemporaryFiles — system.TemporaryDirectory
Рассмотренный выше способ работы с файлами каждый раз перезаписывает файл при записи и читается весь файл целиком, что может быть полезным если вы хотите хранить в файле настройки вашей игры, но у данного метода есть один существенный недостаток — для сохранения настроек их придется как-то форматировать и что бы в дальнейшем при открытии для чтения можно было их обратно прочесть, если у вас не много настроек и нет вложенных таблиц, то скорее всего особых проблем не возникнет (но это все равно не удобно) если же структура ваших настроек достаточно сложная удобней будет использовать в качестве инструмента сериализации формат json (ссылка для тех кто не знает о чем речь, но хочет узнать). Последовательность наших действий будет такой:
- Все настройки проекта храним в таблице config
- Перед записью в файл переводим содержимое config в json
- Записываем в файл
- Для чтения настроек открываем файл считываем все содержимое файла в переменную string
- Переводим из json в формат lua таблицы
- Инициализируем config
Ниже приведу пример простого проекта, который реализует описанную выше идею, вы легко сможете перенести этот механизм в свои проекты так как для целей данного занятия была изготовлена универсальная библиотека, в которой вам потребуется для адаптации к своему проекту только заполнить шаблон настроек по умолчанию, ну и по желанию изменить имя файла настроек.
Код библиотеки (libLoadSave.lua)
-----------------------------------------
-- Библиотека для работы с настройками --
-----------------------------------------
local M = {
template = {--шаблон настроек
--значения по умолчанию
num = 1,--количество запусков
time = 0,--время последнего запуска
},
config_file = system.pathForFile('saves.dat', system.DocumentsDirectory )--путь к файлу
}
local json = require "json"--подключаем к проекту библиотеку json
--сохранение настроек на диск,
--если файла нет - создание шаблонных настроек
M.save = function()
local file = io.open(M.config_file, "w")--открываем файл для записи
if file then--если открытие прошло успешно
local write_data = json.encode(config)--переводим настройки в json
file:write( write_data )--запись в файл
io.close( file )--закрытие файла
end
end
--загрузка настроек из файла
M.load = function()
local file = io.open( M.config_file, "r" )--открытие файла для чтения
--если файл существует
if file then
local read_data = file:read("*a")--читаем файл целиком
config = json.decode(read_data)--переводим из json в lua, инициализируем config
io.close( file )
--если файла нет
else
config = M.template--приравниваем настройки шаблону
M.save()--создаем файл с шаблонными настройками
end
end
return M
Порядок применения библиотеки следующий:
- Создать глобальную config
- Подключить к проекту библиотеку
- Вызвать в начале кода загрузку настроек
- Если были изменены настройки и их нужно сохранить вызываем функцию сохранения
Рассмотрим исходник основного файла реализующего описанный механизм:
--main.lua
config = {}--хранилище настроек (глобальная переменная - видна во всем проекте)
ls = require "libLoadSave"--подключаем библиоетку
ls.load()--загружаем настройки
--выводим текущее состояние настроек
for k,v in pairs(config) do
print(string.format('%s = %d',k,v))
end
--изменяем настройки
config.num = config.num + 1--увеличиваем количество запусков
config.time = os.time()--время последненго запуска
ls.save()--сохраняем
Создание/Запись/Чтение файла построчно
Рассмотрим вариант работы с файлами при котором будет выполняться следующая последовательность действий:
- Если файла нет — создаем — записываем строку
- Если файл есть дописываем строку в конец файла
local write_data = "Very important entry in the logn"--строка для записи в файл
local fileName = 'log.txt'--имя файла
local filePath = system.pathForFile(fileName, system.CachesDirectory )--путь к файлу
local file = io.open(filePath, "a")--открываем файл, ставим указатель в конец файла, если файла нет - создаем
file:write( write_data )--записываем
io.close( file )--закрываем файл
Так как мы рассматриваем этот пример намереваясь в последующем реализовать лог проекта, то обратите внимание на то что файл я храню в каталоге кеша, так же можно его хранить во временной директории. Это связано с тем что логи имеют наибольшую ценность как аппаратно зависимые данные, например на одном устройстве у вас есть ошибка а на другом нет, и синхронизация логов в общем порядке плохо скажется на их информативности. Хранение логов во временной папке дает возможность избежать существенного дрейфа размера занимаемого приложением (если хранить логи годами), так как при каждой перезагрузке приложения логи будут удаляться (это не всегда так, чаще логи удаляются при рестарте телефона либо в результате работы всяких «чистилок»).
Вторым шагом реализации логов будет рассмотрение способа построчного чтения файла — это пригодится для чтения данных из лога:
local fileName = 'log.txt'--имя файла
local filePath = system.pathForFile(fileName, system.CachesDirectory )--путь к файлу
local file = io.open( filePath, "r" )--открываем файл для чтения
for line in file:lines() do--в цикле читаем все строки
print( line )--печать строки в консоль
end
io.close( file )--закрываем файл
Теперь как и в прошлый раз реализуем удобный для переноса и повторного использования механизм логов. Сначала код библиотеки (libLog.lua):
-----------------------------------
-- Библиотека для работы с логом --
-----------------------------------
local M = {
log_file = system.pathForFile('log.txt', system.CachesDirectory )--путь к файлу
}
--запись в лог
M.write = function(msg)
local file = io.open(M.log_file, "a")--открываем файл для записи в конец
if file then--если открытие прошло успешно
local dt = os.date("%x %X")--дата-время
local write_str = string.format('%s %qn',dt,msg)
file:write( write_str )--запись в файл
io.close( file )--закрытие файла
end
end
--печать лога в консоль
M.read = function()
local file = io.open( M.log_file, "r" )--открытие файла для чтения
--если файл существует
if file then
for line in file:lines() do--в цикле читаем все строки
print( line )--печать строки в консоль
end
io.close( file )--закрываем файл
end
end
return M
Рассмотрим main.lua
--main.lua
LOG = require "libLog"--подключаем библиотеку
--записываем в лог
local write_data = "Very important entry in the log"--строка для записи в файл
for i = 1,10 do
LOG.write(write_data..' - '..i)
end
--выгружаем содержимое лога в консоль
LOG.read()
Дата и время
Во многих развитых языках вроде C# или Delphi работа с датой и временем осуществляется за счет использования десятков готовых функций, которые с одной стороны все равно не могут предусмотреть всего и приходится что-то «допиливать», с другой стороны хрупкий
- os.time() — возвращает UNIX-время, исходя из параметров оно может быть различным
- os.date() — принимает UNIX-время и набор параметров, возвращает либо таблицу со значениями либо формализованную строку, если не указывать UNIX-время будет возвращена таблица текущего время.
Т.о. всего две функции, но используются они очень гибко и это позволяет решить большую часть необходимых задач по работе с датой и временем. Имеется еще одна дополнительная функции косвенно связанная с временем — os.clock, эта функция позволяет определить время выполнения кода. Рассмотрим все эти функции в деталях.
os.time()
В самом простом варианте функция вызывается без параметров и возвращает текущий системный timestamp(UNIX-время / POSIX — время) — это количество секунд, которое прошло с момента начала UNIX-эры (01.01.1970 в 00:00:00UTC).
print(os.time())-->>1513065913 (у вас будет больше)
В расширенном формате функция os.time способна вернуть timestamp любой точки времени после начала UNIX-эры, для этого необходимо передать функции в качестве параметра таблицу в специальном формате: date table. Ни чего «магического» в этой таблице нет ниже привожу ее описание:
year 1970-3000
month 01-12
day 01-31
hour 00-23
min 00-59
sec 00-59
isdst true если летнее время
Т.е. если заполнить эту таблицу и передать ее os.time функция вернет для установленного момента времени timestamp. Параметры год, месяц, день являются обязательными. Так же имеются временные рамки в пределах которых можно вводить параметры они простираются от начала UNIX-эры и до (31.12.3000 23:59:59UTC) если ввести выше и ниже этих пределов функция вернет nil. Пример кода:
print(os.time{year = 1990, day=1, month=1, hour=3, min = 0, sec = 0}) -->> 631152000
print(os.time{year = 2000, day=1, month=1, min = 0, sec = 0}) -->> 946717200
print(os.time{year = 2010, day=1, month=1}) -->> 1262336400
Обратите внимание, параметры времени: час, минута, секунда по умолчанию используются значения 12:00:00. В таком применении функцию os.time удобно использовать для сравнения дат, это значительно удобней чем делать сложный обход по параметрам date table. Можно так же использовать результат этой функции для вычисления «сколько осталось до..» или «сколько прошло с..», единственным неудобством будет то что переводить из секунд в дни часы, минуты и секунды все же придется в ручную, но это не сложно. Приведу пример, который вычислить оставшееся время до Нового Года в днях часа минутах и секундах:
--получаем следующий год
local year = os.date('*t').year + 1--эта "магия" станет понятно совсем скоро
--получаем чсило секунд до НГ
local before_NY = os.time{year = year, month = 1, day = 1, hour = 0} - os.time()
local s_day, s_hour, s_min = 24*60*60, 60*60, 60--количество секунд в сутках, часе, минуте
local day = math.floor(before_NY / s_day)--количество дней
local hour = math.floor((before_NY - day * s_day) / s_hour)--количество часов
local min = math.floor((before_NY - day * s_day - hour * s_hour) / s_min)--количество минут
local sec = before_NY - day * s_day - hour * s_hour - min * s_min--количество секунд
print(string.format('Until the New Year there are %d days %d hours %d minutes %d seconds', day, hour, min, sec))
os.date
Не смотря на название, функция os.date является обратной функцией от os.time, т.е. из низкоуровневого формата timestamp она получает более высокоуровневый формат date table, в возвращаемом результате функции os.date имеются поля которые не требуются функции os.time это поля: yday — день года (1..365 или 366 для високосных годов) и wday — день недели (1 — Воскресенье, 2 — Понедельник… 7 — Суббота). Функция принимает 2 параметра:
- Форматирующая строка — эта строка определяет формат вывода данные, для того что бы получить в качестве результата таблицу data table нужно использовать один из двух ключей "*t" — дата-время возвращаются с учетом вашей временой зоны(т.е. то же время что показывают часы на компьютере), "!*t" — дата-время возвращаются без учета временной зоны, то по времени UTC. Если не использовать ключ, необходимо ввести строку определяющую формат вывода, при этом результат будет возвращен в виде строки, места вставки информации и ее тип определяются тегами которые начинаются символом %, полный список возможных тегов приводится после списка.
- UNIX-время — этот параметр является не обязательным и по умолчанию используется текущее время системы.
- %a Сокращенное английское название дня недели
- %A Полное английское название дня недели
- %b Сокращенное английское название месяца
- %B Полное английское название дня недели
- %c Дата и время формата: 09/16/98 23:48:10
- %d День месяца [01-31]
- %H Час в полном формате [00-23]
- %I Час в формате am/pm [01-12]
- %M Минуты [00-59]
- %m Месяцы [01-12]
- %p «am» или «pm»
- %S Секунды [00-61]
- %w День недели [0-6 = Воскресенье-Суббота]
- %x Дата формат: 09/16/98
- %X Время формат: 23:48:10
- %Y Год полный формат [1970-3000]
- %y Год формат «две цифры»: [00-99]
- %% Символ % в строке форматирования
Переходим к примерам использования os.date:
--[[
ВСПОМОГАТЕЛЬНАЯ ФУНКЦИЯ
]]
--функция выводит содержимое таблицы date table
print_dt = function(t)
local s = ''
for k,v in pairs(t) do--проход по таблице
local vv = type(v)=='boolean'--если параметр boolean
and (v and 'true' or 'false')--формируем строки
or v--если не boolean просто приравниваем v
s = s .. string.format('%s=%s, ',k,vv)--сцепляем строку
end
print(s)
end
--[[
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ os.date С ПОЛУЧЕНИЕ РЕЗУЛЬАТА - ТАБЛИЦЫ
]]
print_dt(os.date('*t'))--текущее время с учетом пояса
-->> hour=21, min=4, wday=3, day=12, month=12, year=2017, sec=14, yday=346, isdst=false,
print_dt(os.date('!*t'))--текущее время без учета пояса
-->> hour=18, min=4, wday=3, day=12, month=12, year=2017, sec=14, yday=346, isdst=false,
print_dt(os.date('!*t',1000000000))--дата-время в миллиардную секунду UNIX-эры
-->> hour=1, min=46, wday=1, day=9, month=9, year=2001, sec=40, yday=252, isdst=false,
--[[
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ os.date С ПОЛУЧЕНИЕ РЕЗУЛЬАТА - СТРОКИ
]]
print(os.date('Now: %c'))--Now: 12/12/17 21:04:14
print(os.date('Date: %x'))--Now: 12/12/17
print(os.date('Time: %X'))--Now: 21:04:14
print(os.date('2^9 sec UNIX-time: %c',2000000000))--Now: 2^9 sec UNIX-time: 05/18/33 06:33:20
os.clock
Часто в процессе создания приложения возникают проблемы с производительностью и не всегда просто понять какой конкретно фрагмент кода сильно нагружает все приложение, или например приводит к долгим запускам программы. Так же можно с помощью функции выбирать какой вариант реализации одной и той же функции выбрать. Функция os.clock — возвращает время в секундах (точность до 4 знака после запятой), которое потратил процессор на работая над приложением с момента старта приложения. Т.е. если свернуть приложение в память и не использовать его — отсчет времени будет приостановлен до возврата к приложению. Пример кода сравнивающего эффективность двух вариантов выполнения одной задачи, пример естественно шуточный (все и без тестов угадают что работает быстрей).
--сравним как эффективней считать сумму чисел ряда 1..N
func1 = function(N)--годная функция
return (N*(N+1))/2
end
func2 = function(N)--реализация "в лоб"
local sum = 0
for i=1,N do
sum = sum + i
end
return sum
end
local N = 1000000000--вычисляем сумму чисел от 1 до N
local x = os.clock()
func1(N)
print('funt1: '..(os.clock() - x)..' sec')-->> funt1: 0 sec
x = os.clock()
func2(N)
print('funt2: '..(os.clock() - x)..' sec')--> funt2: 6.293 sec
Функции
В процессе изучения этой статьи я не раз говорил о функциях и некоторые примеры имели их на борту, но качественный разбор этого понятия я решил перенести к концу статьи, так как имеющийся сейчас базис лучше подходит для пониманию некоторых тонкостей. Начнем с теории. Функция — фрагмент программного кода (подпрограмма), к которому можно обратиться из другого места программы. В большинстве случаев с функцией связывается идентификатор, но многие языки допускают и безымянные функции.
Анонимные функции
Lua допускает наличие безымянных (обезличенных, анонимных) функций — это повсеместно применяется в том случае, если одна функция требует в качестве одного из аргументов другую функцию и если этот фрагмент вам больше ни где не пригодится вы с легкой душой создаете анонимную функцию. Приведу пример:
func1 = function(func)--функция требует функцию в качестве параметра
func()--запуск переданной в параметре функции
end
--вызываем функция func1
func1(function()--реализуем анонимную функцию
print('Hi from an anonymous function!')
end)
Порядок объявления функций
Вы скорее всего уже заметили, но все же подытожу, функции в Lua можно объявлять двумя способами:
Присвоение ее значения переменной
name1 = function()
end
Непосредственное объявление
function name2()
end
Вы можете использовать любой способ, но лучше первый, так как небольшая разница есть — на момент инициализации модуля(файла) в нем единовременно выполняется весь локальный контекст, т.е. если вне функций имеются какие-то присвоения переменных, они будут один раз присвоены(функция объявленная как переменная будет инициализирована), если же объявить вторым путем функция будет инициализирована при первом использовании.
Порядок вызова функции
Функция вызывается путем указания ее имени и в скобках передачи параметров, если не указывать скобки функция просто будет хранить свой тип данных и адрес вызова, ты вы можете просто напечатать в print функцию и она при этом не вызовется:
function func1()
print('Hi')
end
print(func1)-->> function: 005DBA00 (у вас возможно будет другой адрес)
print(func1())-->заметьте тут ни чего не вернулось - печатать не чего
Второй print ни чего не печатал, так как функция не имеет возвращаемых параметров. Есть еще один вариант организации вызова функции. В функциях может требоваться множество входных параметров и их можно либо передавать через запятую либо организовать функцию так что бы на входе она ожидала всего один параметр — table, а в нем в формате ключ = значение размещены все параметры:
--функция со списком параметров
func1 = function(a,b,c)
print(a,b,c)
end
--функция с таблицей парметров
func2 = function(p)
print(p.a,p.b,p.c)
end
--вызов func1
func1(1,2,3)-->> 1 2 3
--вызов func2
func2{-->> 4 5 6
a = 4,
b = 5,
c = 6
}
На первый взгляд может показаться что 2 способ «какой-то длинный — писать много», но на практике он на много удобней, приведу аргументы в защиту «длинного способа», возможно удастся вас убедить что он хорош:
- Если у вас есть только третий параметр то функция первого типа потребует от вас забить nil-ами остальные параметры что бы дойти до 3 параметра func1(nil,nil,3), для второго способа все проще func2{c = 6}
- Используя первый способ вам нужно иметь перед глазами реализацию функции что бы не забить в какой последовательности нужно вводить значения, во втором случае это не имеет ни какого значения и так то же будет работать func2{c = 6, b = 5, a = 4}
- Если параметров много то вторым способом можно их передать предварительно разместив в отдельную таблицу и далее сделать аккуратный вызов, не нарушающий стройность функции, в первом же случае вы всегда будете обязаны делать долгий вызов
- Для второго случая вы можете иметь шаблон параметров в виде таблицы и перед передачей параметров просто заполнять те значения которые вас не устраивают в шаблоне по умолчанию, для первого варианта — только как всегда.
Хочется верить, что ваш выбор в случае если параметром в функции более одного всегда будет склоняться ко второму варианту, но решать только вам.
Возврат результатов
Тут все просто, что бы функция что-то вернула нужно в тот момент когда результат получен использовать оператор return после которого будет идти переменная (или несколько переменных). Как и в случае передачи параметров функции, так и в случае возврата результатов выполнения я бы вам рекомендовал использовать результирующие таблицы в которых размещены все результаты:
--функция со списком параметров
func1 = function(a,b,c)
return a+b, b+c, c+a--результат списком
end
--функция с таблицей парметров
func2 = function(p)
return{ p1 = p.a+p.b,--результат таблицей
p2 = p.b+p.c,
p3 = p.c+p.a}
end
--вызов func1
val1, val2, val3 = func1(1,2,3)
print(val1, val2, val3)-->> 3 5 4
--вызов func2
val = func2{
a = 4,
b = 5,
c = 6
}
print(val.p1, val.p2, val.p3)-->> 9 11 10
Регулярные выражения и захваты
С понятиями регулярных выражений и захватов вы сталкивались при изучении этой статьи уже несколько раз, в частности при изучении следующих функций: string.match, string.gmacth, string.sub, string.gsub, string.find. Как же это работает? Приведу самую малость терминов.
Регулярное выражение — это строка состоящая из последовательности символов определяющая шаблон разбора текстовой строки на составные части.
Регулярное выражение может состоять из управляющих конструкций, непрерывных последовательностей символов и захватов.
Управляющая конструкция — это шаблон регулярного выражения который может заменять собой один или несколько символов, т.е. например %d — это любое число. Полный список управляющих конструкций представлен ниже:
- . — любой символ
- %a — латинская буква
- %с — контрольный символ
- %d — десятичная цифра
- %u — латинская буква верхнего регистра
- %l — латинская буква нижнего регистра
- %p — знак пунктуации
- %s — символ пробела
- %w — латинская буква или арабская цифра
- %z — нулевой символ
- %A — не латинская буква
- %C — не контрольный символ
- %D — не десятичная цифра
- %U — не латинская буква верхнего регистра
- %L — не латинская буква нижнего регистра
- %P — не знак пунктуации
- %S — не символ пробела
- %W — не латинская буква и не арабская цифра
- %Z — не нулевой символ
Имеется возможность несколько управляющих конструкций объединить в одну, для этого их необходимо поместить в квадратные скобки "[]". Обратите внимание, что для большинства управляющих конструкций имеется 2 взаимоисключающих варианта например %w и %W на самом деле инвертировать любую конструкцию можно символом крышки "^", т.е. ^%w аналог %W и наоборот ^%W аналог %w. Так же большую часть стандартных управляющих конструкций можно организовать вручную например для %d аналог [0-9], для %w — [0-9a-zA-Z], для краткости там где это возможно лучше применять зарезервированные стандартные конструкции.
Непрерывная последовательность — это строгая последовательность символов, которая должна быть в строго определенном месте регулярного выражения. Приведем пример есть строка «param1: 1234, param2: 54321», используем следующее регулярное выражение для ее разбора "%d+"(означает любое количество идущих подряд символов, но не менее 1). Это регулярное выражение найдет строку «1234» и функция вернет результат, если же нам нужно получить значение именно второго параметра мы можем либо запустить поиск второй раз раз (используя соответсующий параметр функций), либо просто указать непрерывную последовательность символов, а именно «params2: » и регулярное выражение будет иметь вид: " params2: %d+", а если пойти дальше и это упростить откинув лишнее то останется «2: %d+». Стоит заметить что после уменьшения длины непрерывной последовательности увеличится универсальность регулярного выражения и при большем количестве параметров может произойти промах.
Захват — это помещение в переменную определенных частей строки путем разбора его регулярным выражением. Что бы организовать захват достаточно поместить некоторую часть регулярного выражения в круглые скобки "()". В одном регулярном выражении может быть множество захватов. Пример захвата: имеется исходная строка "ip:123.22.345.23", необходимо извлечь все 4 числовые части ip адреса, применим следующее регулярное выражение в качестве аргумента функции string.match «ip:(%d+)%.(%d+)%.(%d+)%.(%d+)»код будет выглядеть примерно так:
local st = "ip:123.22.345.23"
local reg = "ip:(%d+)%.(%d+)%.(%d+)%.(%d+)"
local d1,d2,d3,d4 = string.match(st, reg)
print(d1,d2,d3,d4)-->> '123' '22' '345' '23'
Обратите внимание, что символ точки между захватами необходимо экранировать, так как этот символ является управляющей конструкцией и означает "любой символ", если этого не сделать числа можно будет разделять любым символом а захват все равно будет работать. Имеются и другие символы, который в теле регулярного выражения необходимо экранировать вот их список:
( ). % + — *? [ ] ^ $
ВАЖНО: В регулярных выражениях имеется строгая необходимость выполнения всех условий, т.е. если не выполнено хотя бы одно условие разбора строки весь захват не выполняется, и с все переменные получают значение nil.
Как сделать захваты более универсальными, например оставить возможность что третье число ip по какой-то причине будет отсутствовать? На самом деле все просто — сейчас мы используем в "+" в паре с управляющей конструкцией %d, а это предполагает наличие хотя бы одного числа, если убрать +, то условие будет такое — должен быть строго один символ, если же вместо "+" поставить символ "*" то условие будет — захват любого количества чисел включая ни одного. Проверяем:
local st = "ip:123.22..23"
local reg = "ip:(%d+)%.(%d+)%.(%d*)%.(%d+)"
local d1,d2,d3,d4 = string.match(st, reg)
print(d1,d2,d3,d4)-->> '123' '22' '' '23'
Все отлично сработало. Переходим к следующему усложнению, что если в строке содержится число в шестнадцатиричном формате, т.е. по мимо цифр могут содержать символы abcdefABCDEF. Для этой задачи объединим несколько управляющих конструкций в одну с помощью "[]", управляющие знаки +* нужно вынести за квадратные скобки, или они то же будут считаться часть суммарной конструкции. Что бы не перечислять все символы(если есть символы идущие подряд) можно их можно сгруппировать и перечислить через дефис следующим образом «a-fA-F»
local st = "code = 12f3a2e2ffd423A;"
local reg = "code = ([a-fA-F%d]+)"
local code = string.match(st, reg)
print(code)-->> '12f3a2e2ffd423A'
Теперь рассмотрим вариант упрощение этой регулярки. Попробуем идти от обратного т.е. искать не определенные символы, а захватывать все символы кроме завершающего символа, в данном случае ";". Как выше писалось, что бы инициировать инвертирование «все кроме» необходимо поставить символ крышки "^" перед управляющим символом. Так же для конкретно этого случая можно сильно упростить непрерывную последовательность перед захватом всего до "= ", если это не сделать будет захвачен первый символ строки «code» так как он тоже отвечает регулярной последовательности, а если поставить только пробел " " будет захвачен лишний фрагмент "= ".
Смотрим код:
local st = "code = 12f3a2e2ffd423A;"
local reg = "= ([^;]+)"
local code = string.match(st, reg)
print(code)-->> '12f3a2e2ffd423A'
Внутри захвата могу присутствовать как последовательности символом на несколько отдельных или сгруппированных управляющих конструкций, т.е. захваты такого вида тоже корректны:
"(buka[%d]+%s*%.[%w]*[^%+]+)".
Регулярные выражения в Lua являются серьезной часть системы и не редко позволяют значительно упростить многие вещи. Приведу несколько советов по порядку применения регулярных выражений:
- Старайтесь делать регулярку самым простым и очевидным способом
- Всегда комментируйте что делает фрагмент с регулярным выражением
- Если в регулярном выражении возникла ошибка и вы знаете что оно должно делать, лучше написать его заново чем пытаться исправлять
На последок приведу пример функции делающей «слабую» проверку валидности email.
--проверка валидности email
check_email = function(email)
local a,b,c = string.match(email,'([^@ ]+)@([^%.][^%.]+)%.(..+)')
return a ~= nil and b ~= nil and c ~= nil
end
Это выражение работает по следующему правилу:
- 1 захват — должен идти как минимум один символ не "@" — имя пользователя
- символ "@"
- 2 захват — должно идти как минимум два символа не "." — домен 2 уровня
- символ "."
- 3 захват — как минимум один любой символ — домен 1 уровня
На первый взгляд есть масса способов ввести плохой email, но на практике это допустимая строгость проверки этого параметра.
Управление случайностью
Очень часто в реальных приложениях требуется внести некоторую случайность, например выбрать одно из заранее определенных значений или задать случайное направление, цвет и т.д. Для решения этих задач в большинстве языков имеется генератор псевдослучаных чисел.
Математически создать по настоящему случайную последовательность, пока что считается невозможным, есть аппаратные решения решающие эту задачу, но это отдельная история. В Lua генератор, для получения псевдослучайных чисел используется функция math.random(). Генератор действует по определенному алгоритму раз в какое-то время последовательность начинает повторятся, длина последовательности может быть достаточно большой, но есть минус каждый раз при запуске приложения последовательность начинается заново, т.е. если к примеру, при входе в приложение программа будем получать случайное число от 1 до 100, то каждый раз мы будем видеть один и тот же результат, если поставим это действие на вход в приложение:
print(math.random(1,100))--первое случайное число 1..N всегда 1)
Всегда будет получаться 1, это безусловно очень неудобно, в реальных задачах. В попытке решить эту проблему можно воспользоваться второй функцией предназначенной для управления math.random, эта функция math.randomseed — с помощью нее мы можем изменить первый шаг случайной последовательности и генерация будет начинаться не с единицы, а с другого (всегда одинакового) числа.
local seed = 1234567890
math.randomseed(seed)
print(math.random(1,100))--первое случайное число 1..N всегда 4)
Теперь каждый раз при входе в приложение мы видим число 4. Что-то опять не так! Очевидно на вход генератора через math.randomseed нужно подавать не всегда один и тот же seed. Как это получить? Можно попробовать в качестве seed использовать уже известную функцию os.time(), в этом случае если входить в приложение реже чем раз в секунду мы будем получать разное число:
math.randomseed(os.time())
print(math.random(100))--значение меняется если с прошлого запуска прошла 1 секунда
Если же хочется что бы смена seed происходила примерно в 10000 быстрей, можно воспользоваться таким способом:
socket = require( "socket" )
local seed = (socket.gettime()*10000)%10^9
math.randomseed(seed)
print(math.random(100))--значение меняется если с прошлого запуска прошло более 100 микросекунд
Любознательный читатель разберет это пример сам.
На последок рассмотрим все варианты применения math.random
local n,m = 100,200
math.random(n)--число от 1 до n
math.random(n,m)--число от n до m
math.random(-m,m)--начало периода может быть отрицательным
math.random(-m,-n)--начало и конец периода могут быть отрицательными, но конец должен быть ближе к нуля
math.random()--генерируется случайное вещественное число от 0 до 1 (до 16 знаков после запятой)
О БУДУЩЕМ: В процессе изучения Corona SDK мы будем экспериментировать с внесением энтропии в механизм генерации чисел, например получая сигналы флюктуаций с датчика акселерометра или других датчиков, это способ является по настоящему случайным, но не является математическим и так как время квантования (опроса) у всех датчиков составляет килогерцы, а выполнения цикла for (с небольшим числом команд) может занимать значительно меньше времени, проблемы с этим способом будут не менее существенными, нам придется либо замедлять цикл до времени квантования датчика, либо использовать дополнительные источники энтропии.
Хочется отметить, что в вопросах создания очень уж реалистичной случайности не стоит сильно себя терзать, даже знатный трудяга-математик Джон фон Нейман говаривал:
«Всякий, кто питает слабость к арифметическим методам получения случайных чисел, грешен вне всяких сомнений.»
Математические функции
В предыдущей разделе были описаны функции random и randomseed из библиотеки математических функций math. В этой библиотеке есть и другие функции которые рано или поздно вам пригодятся.
- abs(x) Возвращает модуль числа x.
- ceil(x) Возвращает наименьшее целое число, большее или равное заданному x (выполняет округление «вверх»).
- floor(x) Возвращает наибольшее целое число, меньшее или равное заданному x (выполняет округление «вниз»).
- max(....) Возвращает максимальный из аргументов(принимает множество аргументов).
- min(....) Возвращает минимальный из аргументов(принимает множество аргументов).
- fmod(a,b) Возвращает остаток от деления a на b.
- modf(f) Возвращает целую и дробную части исходного числа f.
- frexp(x) Возвращает нормализованную мантиссу и показатель аргумента. x = m2e
- ldexp(m,e) Строит число по мантиссе и показателю. m2e (e должно быть целым)
- pow(x,y) Возводит число x в степень y. Вместо вызова функции возможно использование выражения вида x^y.
- sqrt(x) Вычисляет квадратный корень числа x. Вместо вызова функции возможно использование выражения вида x^0.5.
- exp(x) Возвращает ex.
- log(x) Вычисляет натуральный логарифм x.
- log10(x) Вычисляет логарифм x по основанию 10.
- cos(x) Вычисляет косинус угла x, заданного в радианах.
- sin(x) Вычисляет синус угла x, заданного в радианах.
- tan(x) Вычисляет тангенс угла x, заданного в радианах.
- cosh(x) Вычисляет гиперболический косинус.
- sinh(x) Вычисляет гиперболический синус.
- tanh(x) Вычисляет гиперболический тангенс.
- acos(x) Вычисляет арккосинус (в радианах).
- asin(x) Вычисляет арксинус (в радианах).
- atan(x) Вычисляет арктангенс (в радианах).
- atan2(x,y) Возвращает арктангенс x/y (в радианах), но использует знаки обоих параметров для вычисления «четверти» на плоскости. Также корректно обрабатывает случай когда y равен нулю.
- deg(x) Переводит величину угла из радиан в градусы.
- rad(x) Переводит величину угла из градусов в радианы.
По мимо функций в библиотеке math имеется еще и 2 константы — math.pi и math.huge. С math.pi мы уже знакомились еще в первой части статьи и это просто константа хранящая всем известную переменную (Пи), точность константы достаточна для большинства случаев — 3.1415926535898. Константа math.huge более загадочная сущность ее особенность — она всегда больше или равна любого числового значения.
Бережное отношение к чужому...
В этой кратком и заключительном раздел мы поговорим вовсе не о том, что к машине взятой на прокат стоит относиться как к своей, речь пойдет о стандартных функциях языка. В этот момент я передумал добавлять в статью еще 2 раздела и удалил их, очень хочется закончить и выспаться:(. Во многих языках достаточно трудно испортить стандартные функции, в Lua — нет ни чего проще. Присвоим любой функции произвольное значение. Приведу пример:
print = 1--переинициализируем значение print
print('Hello World')--ошибка "attempt to call global 'print' (a number value)"
Как видите испортить то не многое, что есть в этом языке очень просто. Зарезервированные слова вроде end,begin,then таким образом не испортить, а вот остальные функции совершенно беззащитны перед проявлением мелкой беспечности. Такие ошибки будет очень не просто найти, особенно если в проекте десятки тысяч строк и над ним работает куча людей. Могу лишь дать один совет всегда используйте редакторы кода с подсветкой кода Lua и желательно функций Corona SDK, и если то что вы решили сделать именем переменной или функции где-то уже используется — придумайте новое имя.
Если пример с инициализацией print вам кажется несколько "притянутым за уши", приведу гораздо более реальный пример, который несет еще большие последствия и выявляется еще сложней.
Разработчик решил ввести в программе логический флаг присвоение истины(true) которому, будет разрешать печать в консоль отладочной информации. Не долго думая он решил что всякие транслит названия (вроде OTLADKA) для его проекта совершенно не годятся и назвал переменную debug. Присвоил переменной true, проверил что все работает и продолжил дальше разрабатывать, через час, день или больше кто-то из его коллег заметил что Corona SDK совсем сломалась и совершенно не выдает сообщения об ошибке, даже если пишешь в коде полный бред. Оказалось, что функция debug — это стандартная функция Lua, которая мало того что может пригодиться вам в отладке так еще и используется самой короной и ее устранение полностью ломает систему отладки ошибок проекта. Я думаю, это достаточно хороший пример подтверждающий идею, что необходимо бережное отношение к чужому…
Заключение
Если вы изучили все три части статьи то вы получили большую часть того что вам потребуется знать что бы уверенно создавать свои проекты на Corona SDK. Некоторые вопросы я рассмотрел слишком подробно, другим возможно уделил не достаточно внимание, где-то возможно я был не прав или не точен, но хочется верить что будут люди которым эта информация поможет. Многие вещи из дополнительных библиотек языка остались за рамками этой статьи это связано с тем что некоторым функциям Lua есть более приемлемая замена из функций Corona SDK. В следующих статьях мы будем изучать уже не Lua, а именно Corona SDK, т.е. тот функционал которого в чистом виде в языке нет. Вас непременно ждет множество статей в которым мы будем писать игры и делать другие полезные вещи.
Хочется выразить благодарность за помощь оказанную в написании статьи, тем людям которые комментируют и указывают на неточности, так же разработчикам движка, с которыми я имел честь общаться и консультироваться через их группы в соцсетях(VK и facebook) и Slack-канал(вход свободный).
Всем спасибо и удачи! С уважением, Денис Гончаров aka execom
email: x@x-id.ru
PS: Спасибо и тем кто все это прочитал.
Автор: Денис Гончаров