Lua стандартные функции. Объекты в Lua. Расширенная форма оператора for

Для Андроид 26.04.2019
Для Андроид

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

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():Понравилось это.

Часть 2 : Разобравшись с базовыми возможностями Lua , Андрей Боровский пробует эмулировать в нем конструкции, знакомые по другим языкам.

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

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

Функции Lua

Давайте рассмотрим такую коротенькую программу:

Function foo() print(“Привет, я - функция foo()!”) return 1 end print(foo()) print(foo)

Первые четыре строки в пояснениях особо не нуждаются. Ключевое слово function объявляет функцию. Далее следуют ее имя и список аргументов, заключенный в скобки (у нас он пуст). Тело функции – это блок, обязанный заканчиваться ключевым словом end . Обратите внимание, что хотя наша функция возвращает значение 1, оператором return , тип возвращаемого значения в ее объявлении не указывается. Аналогично тому, как одна и та же переменная Lua может принимать значения любых определенных в языке типов, одна и та же функция Lua может возвращать значения всех возможных типов. Все-таки Lua не зря назван именем небесного тела, обозначающего в символике многих народов переменчивость и обманчивость. В пятой строке мы распечатываем значение, возвращаемое foo() (при этом, естественно, выполняется сама функция foo() ). Шестая строка выглядит интереснее. В нашем фрагменте, foo – это переменная, содержащая значение типа «функция» (на самом деле – идентификатор функции, но об этом ниже). В шестой строке мы печатаем значение переменной foo , а не результат, возвращаемый функцией. Вот что мы получим:

Привет, я - функция foo()! 1 function: 00379B20

Первые две строки вывода – результат выполнения выражения print(foo()) . Последняя строка показывает содержимое переменной foo . Слово function свидетельствует о том, что она содержит идентификатор функции. Далее следует само значение идентификатора (в нашем случае – 32‑битное шестнадцатеричное число). Возникает соблазн назвать идентификатор адресом функции, но следует помнить, что концепция адресов и указателей в Lua отсутствует.

Для завершения примера приведем определение функции, которая принимает параметры:

Bar = function(a, b) print(“a+b=”.. a + b) end bar(2,3)

Конструкция

Bar = function(a, b)

эквивалентна

Function bar(a, b)

как, например, в JavaScript . А вот еще один интересный момент:

Function baz() return 1, true, “три” end a,b,c = baz() print(a,b,c)

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

Итераторы

Скажи я вам, что в Lua нельзя объявить функцию с переменным числом параметров, вы бы наверняка удивились. Увы, удивить мне вас нечем: такие функции в Lua существуют:

Function sum(...) r = 0 for i, v in ipairs(arg) do r = r + v end return r end print(sum(1,2,4,8,16,32))

В этом примере много новых элементов. Троеточие в заголовке функции означает, что число принимаемых аргументов может быть любым. Для передачи переменного числа аргументов используются таблицы, которые, напомню, представляют собой основу всех сложных типов данных в Lua . Увидев троеточие, интерпретатор Lua автоматически создает таблицу arg , содержащую пары «номер – значение аргумента». Нумерация аргументов начинается с единицы и продолжается непрерывно, так что выражение arg возвращает первый аргумент, arg – второй, и т. д.

Вспомнив определение оператора # (LXF122), вы поймете, что выражение #arg вернет число аргументов функции. Однако разработчикам Lua этого показалось мало, и в таблице arg есть еще одно поле с индексом n , которое содержит число аргументов, так что вместо #arg можно (и предпочтительно) использовать arg.n .

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

For <список переменных> in <итератор, данные> do ... end

Функции-итераторы служат для последовательного перебора элементов таблицы и могут использоваться не только в операторе for . В нашем примере мы используем встроенную функцию ipairs() , которая, будучи совмещена с циклом for , последовательно заполняет две переменные парами значений «индекс аргумента – его значение» (в нашем примере i содержит индекс элемента arg , а v – значение индексированного элемента). В результате переменная v последовательно принимает значения всех аргу-ментов (т. е. элементов таблицы arg ). У функции ipairs() есть брат-близнец pairs() , который оперирует парами «ключ–значение», а не «индекс–значение» (см. врезку).

Расставим точки над ‘i’

Если в рассмотренном нами примере итератор ipairs() заменить на pairs() , результат выполнения функции sum() будет другим. Дело в том, что ipairs() перебирает только индексируемые элементы массива, тогда как pairs() учтет и arg.n . Значение этого элемента в нашем примере равно 6, так что вместо ожидаемой суммы 63 мы получим 69.

Теперь вам явно хочется написать собственный итератор! Давайте реализуем итератор bpairs() , перебирающий элементы массива arg в обратном порядке. Как ни странно, для этого потребуется объявить не одну, а две функции:

Function backwards(table, count) count = count - 1 if table then return count, table end end function bpairs(table) return backwards, table, #table+1 end

Аргументами функции backwards() должны быть таблица table и значение count , равное количеству индексируемых элементов плюс 1. Внутри самой функции значение count уменьшается на 1, и возвращается это уменьшенное значение и соответствующий ему элемент таблицы. Так будет происходить до тех пор, пока table не окажется равным nil . Если вам кажется, что с функцией backwards() не все так просто, читайте врезку.

Повышенная передача

Параметры-переменные функций Lua передаются не по значению, а по ссылке. Таким образом, изменение значения любого аргумента внутри функции приводит к изменению этого значения и за ее пределами. Этим фактом мы и пользуемся в функции backwards() .

Функция bpairs() работает и того проще. Она возвращает три вещи: саму функцию backwards() и значения аргументов для ее первого вызова. Оператор for вызывает функцию backwards() , используя «для затравки» значения, полученные от bpairs() , до тех пор, пока backwards() возвращает результат. Если вы не поняли это место, не пугайтесь: сейчас будет еще один наглядный пример. Теперь мы можем заменить строку

For i, v in ipairs(arg) do

For i, v in bpairs(arg) do

Аргументы функции sum() будут перебираться в обратном порядке, в чем можно убедиться, вставив в цикл вызов print(i,v) . Сам результат от перемены мест слагаемых не изменится.

Зная, как работают функции-итераторы, мы можем воспроизвести механику оператора for и без обертки bpairs() :

For i, v in backwards, arg, #arg+1 do r = r + v end

For i, v in backwards, arg, arg.n+1 do r = r + v end

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

Чего только нет

Как и в любых других блоках, в теле функции можно объявлять локальные переменные, видимые только внутри нее. В отличие от C/C++ , эти переменные нельзя объявить статическими, то есть сделать так, чтобы они хранили данные в перерывах между вызовами функции. Впрочем, статические локальные переменные можно эмулировать. Вот одно из возможных решений:

Do local loc=0 function fred(a) loc=loc+a return loc end end

Переменная loc объявлена как локальная, и за пределами блока do...end видна не будет. Функция fred() , напротив, не локальная, и ее можно вызывать за пределами блока. Поскольку переменная loc объявлена вне блока функции fred() , она будет существовать в перерывах между вызовами fred() , но поскольку loc локальна для блока, в котором определена функция fred() , никто, кроме fred() , не сможет получить к ней доступ.

Нет в синтаксисе Lua и концепции параметра со значением по умолчанию (как в C++ ), но и тут нам на помощь приходит хакерская изобретательность:

Function defval(v) v = v or "default value" return v end print(defval()) print(defval(‘Мое значение’))

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

V = v or "default value"

можно перевести так: если v не равно nil , присвоить v значение v , иначе присвоить v значение "default value" . Оператор or ведет себя здесь не так, как при работе с логическими значениями, а как краткая форма if . Таким образом, если при вызове defval() мы не указываем v , в теле функции ему назначается значение по умолчанию. В противном случае используется значение, переданное через v .

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

Например, строка

Backwards = nil

удаляет функцию backwards() . Тут, правда, есть один тонкий момент. Рассмотрим фрагмент

Foo = backwards

После первого присваивания идентификатор foo можно использовать так же, как идентификатор backwards . Например:

For i, v in foo, arg, arg.n+1 do

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

Backwards = nil

переменная backwards перестанет указывать на функцию, а foo – не перестанет. В результате память, занятая функцией, освобождена не будет. Уследить за тем, чтобы ни одна переменная не содержала идентификатор функции (а только в этом случае произойдет ее удаление) очень сложно. Эту задачу выполняет автоматический сборщик мусора. Контрольный вопрос: при каких условиях сборщик мусора сможет удалить переменную loc из примера с функцией fred() ? Ответ: когда будут удалены все ссылки на fred() .

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

Объекты в Lua

Родные объекты в Lua отсутствуют, и нам придется их эмулировать. Гибкость синтаксиса это позволяет, но прежде необходимо понимать основы реализации ООП в других языках. В первом приближении, объект – это совокупность структур данных и методов для оперирования ими. Если структура данных и набор методов у двух объектов совпадают, эти объекты могут принадлежать (а могут и не принадлежать) одному классу. Как правило, в программе используется несколько объектов одного класса. Для каждого из них создается своя область данных (чем же иначе объекты будут отличаться друг от друга?), но для ее обработки у всех объектов одного класса используются (физически) одни и те же методы. Каким образом метод, который мало чем отличается от обычной функции, узнает, с какой именно структурой данных ему предстоит работать? Для этой цели у него есть скрытый параметр (в одних языках он называется this , в других – self , в третьих – dontuseme ), который представляет собой указатель на структуру данных того объекта, для которого вызывается метод.

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

С учетом изложенного выше, давайте рассмотрим определение объекта Employee (сотрудник).

Employee = {name = , age = 0, salary = 0, position = } function Employee.incAge(self) self.age = self.age + 1 end function Employee.scaleSalary(self, factor) self.salary = self.salary*factor end function Employee.print(self) print(self.name, "age: "..self.age, "salary: "..self.salary, "position: "..self.position) end; Employee.name = "Vasya Pupkin" Employee.age = 25 Employee.position = "Manager" Employee.salary = 1000 Employee:incAge() e = Employee Employee = nil e:scaleSalary(2); e:print()

У объекта (таблицы) Employee есть четыре поля данных, имена которых говорят сами за себя. Кроме того, для объекта Employee определено три метода: incAge() , увеличивающий значение поля age на единицу, scaleSalary() , умножающий поле salary на заданный коэффициент (желательно – больший единицы) и print() , выводящий сведения о сотруднике.

Обращаю ваше внимание на то, что конструкция

эквивалентна

Employee.print = function (self)

Мы просто создаем еще один элемент таблицы Employee со значением типа «функция». Практически все элементы синтаксиса в представленном фрагменте вам уже знакомы. Новшеством является только выражение типа

Employee:incAge()

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

Employee.incAge(Employee)

Однако такая форма записи более громоздка и не всегда верна: например, она не сработает при использовании полиморфизма.

Оператор: применим не только при вызове, но и при объявлении методов. Например, вместо

Function Employee.print(self)

можно написать

Function Employee:print()

Employee.setName(self, name)

использовать

Employee::setName(name)

Параметр self для функций будет создан автоматически.

Рассмотрим четыре последних строки программы. Переменной e присваивается ссылка на объект Employee , а Employee устанавливается в nil . Тут демонстрирует свою полезность параметр self : будь в методах объекта Employee зашита ссылка на Employee , после выполнения операций они перестали бы работать (ведь переменная Employee будет содержать nil ). Параметр self позволяет использовать методы объектов, не заботясь об имени переменной, которой присвоена ссылка на объект.

Ну, а как создавать экземпляры объекта Employee ? Для этого задействуем мета-таблицы. Мета-таблицами в Lua именуются таблицы, описывающие правила обращения с некоторым значением – в том числе с другими таблицами. Вот как может выглядеть мета-таблица для объектов Employee :

Function Employee:new (name, age, salary, position) obj = {name = name, age = age, salary = salary, position = position} setmetatable(obj, self) self.__index = self return obj end

В результате можно будет написать:

E1 = Employee:new("Vasya Pupkin", 25, 1000, "manager") e2 = Employee:new("Ivan Petrov", 31, 1500, "accountant")

и убедиться, что вызовы e1:print() и e2:print() выдают информацию о двух разных сотрудниках.

Я понимаю, что от синтаксических выкрутасов Lua вы уже готовы лезть на стену. Но, как говорят католики из Рио-де-Жанейро, «Терпение и труд все перетрут». Сейчас мы все поймем.

Метамагия

Прежде всего, new – это обычный элемент-функция таблицы Employee . В ней создается новая таблица obj с четырьмя элементами, значения которых берутся из параметров функции. Строка

Setmetatable(obj, self)

провозглашает, что Employee – мета-таблица для таблицы obj . Теперь при выполнении над obj нестандартных операций (например, индексации несуществующих элементов) таблица obj будет неявно обращаться к мета-таблице Employee за описанием необходимых действий. Еще интереснее строка

Self.__index = self

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

E1:print()

обратится к элементу Employee.print . Благодаря параметру self метод Employee.print будет работать с данными объекта e1 , а не Employee . Кстати, теперь присвоение переменной Employee значения nil аннулирует методы всех объектов, созданных с помощью Employee:new() : ведь их описания исчезнут вместе с мета-таблицей. Как вы уже поняли, при работе с объектами мета-таблица играет роль класса. Стало быть, в Lua можно удалить не только данные объекта, но и код его методов (но вашему преподавателю по C++ об этом молчок).

А можем ли мы написать такое?

Function Employee:new (name, age, salary, position) obj = {name = name, age = age, salary = salary, position = position, print = self.print} setmetatable(obj, self) self.__index = self return obj end

Да, можем, и тогда при вызове метода print() объекту obj не придется обращаться к мета-таблице. Но наш код потеряет гибкость. Если в ходе выполнения программы описание метода print() в мета-таблице изменится, ранее созданные объекты об этом не узнают: ведь у них уже есть свое поле print , и обращаться к мета-таблице им незачем. Можно, наоборот, полностью перенести описание объекта (не только методов, но и полей) в мета-таблицу. Для этого перепишем функцию Employee:new() так:

Function Employee:new (obj) obj = obj or {} setmetatable(obj, self) self.__index = self return obj end

Тогда синтаксис вызова функции Employee:new() тоже изменится:

E1 = Employee:new{name = "Vasya Pupkin", age = 25, salary = 1000, position = "manager"}

Обратите внимание на скобки. Этот вариант кажется неудобным: по сути, объект obj конструируется «вручную» и приходится явно указывать имена полей, уже определенных в мета-таблице. Зато легко организовать наследование классов. Пусть нужно создать объект-потомок класса Employee с переопределенным методом print() . Вот что для этого требуется:

Function newPrint(self) print("name: "..self.name.." age: "..self.age.." salary: "..self.salary.."position: "..self.position) end e3 = Employee:new{name = "Ivan Sidorov", age = 20, salary = 800, position="security manager", print = newPrint} e3:print()

Создавая объект e3 , мы заменяем функцию Employee:print() на newPrint() . В результате при вызове e3:print() на самом деле будет вызвана функция newPrint() – одним махом мы получаем не только наследование, но и, в некотором смысле, полиморфизм. Тем же способом можно добавлять в объекты-потомки Employee новые поля данных и методы, не меняя описания мета-таблицы Employee .

Мы подошли к важной мысли: скрипты Lua способны само-модифицироваться, а значит, быть самообучаемыми! Я не упоминал об этом достоинстве Lua – не буду врать, что просто забыл, скажу честно: новичкам, не прошедшим вторую стадию посвящения, знать о таком было рано. А что же дальше-то будет?! LXF

Всем привет.

Сегодня мы поверхностно пройдёмся по языку Lua, его некоторым возможностям, а так же запуске наших сценариев в RakBot.
Lua - скриптовый язык программирования, предназначен для быстрой обработки данных. С помощью данного языка многие разработчики создают искусственный интелект в играх, пишут алгоритмы генерации уровней, а так же он используется для разработки ресурсов/игровых модов в Multi Theft Auto: San Andreas (аналог SA:MP). На самом деле, это простейший язык и с помощью него мы будем учиться писать собственную логику для ботов, которую будет использовать RakBot.

Пройдёмся по основам программирования, с которыми нам предстоит работать.

Обратите внимание : данная статья будет урезана в плане языка Lua, так как в RakBot используется лишь небольшая её часть. Многие возможности Lua попросту отсустствуют в RakBot, поэтому я буду ориентироваться на версию из RakBot.

Есть традиция у всех авторов книг и документаций различных языков, это первая программа, которая печатает "Hello World".
Чтож, давайте попробуем написать её, но уже в RakBot. Переходим на оффициальный сайт RakBot и ищем раздел "Доступные функции", раздел "События".

Нам необходимо событие onScriptStart() , которые вызывается автоматически при загрузке скрипта самим RakBot"ом.

В этой функции нам необходимо описать логику, которая будет писать в чат-лог RakBot"a "Hello World". Для этого, на той же странице в документации, посмотрим на раздел "Функции".

Первая фукнция printLog(text) - это то, что нам и нужно. С помощью этой функции мы отправим сообщение в чат RakBot"а. Для этого мы напишем:

Мы написали логику в каком-то текстовом документе, но как сказать RakBot, чтобы он выполнил наш сценарий? Для этого необходимо сохранить файл с расширением .lua и положить его в папку scripts , в папке с RakBot.
Я сохранил текстовый документ с именем "example.lua ". Давайте попробуем запустить RakBot и посмотреть, что у нас получилось.

Как мы видим, при запуске RakBot, он находит скрипт "example.lua ", после чего выполняет его. Из этого мы можем сделать вывод, что инициализация сценария происходит при запуске самого RakBot или при перезагрузке всех сценариев командой !reloadscripts .

Поздравляю, Вы только что написали свой собственный сценарий для RakBot!

Мы уже научились писать Hello World в консоли RakBot"а, но мы хотим писать сложных ботов, которые будут делать всю работу за нас, учитывая те или иные условия. На этом мы остановимся.
Практически всё, что происходит в программировании, можно описать следующим образом: возьми данные, что-то с ними сделай, отдай результат.
В данном случае данными выступает сам RakBot. Он сам запускает наши сценарии, а так же сам передаёт нам данные, которые мы можем обработать так, как хотим и в конце получить результат.

Давайте напишем простейший сценарий с условием. Условием будет являться ник бота. Если ник бота "СМaster", значит мы выведем в чат RakBot"а "CM FOREVER", если же ник бота совершенно другой - выведем в чат "Nonamer".
Для этого нам поможет условный оператор if else , он же оператор ветвления. Он принимает на себя условие, которое должно вернуть либо true , либо false . Если условие равно true , тогда код внутри будет выполнен, если false - не будет выполнен.
На этом строится большая часть логики любого приложения. Дословно if переводится как "ЕСЛИ", then - "ЗНАЧИТ", else - "ИНАЧЕ" Если это сильно сложно - не переживайте, Вы поймёте всё дальше.

В Lua есть следующие операторы сравнения:
> Больше
< Меньше
>= Больше или равно
<= Меньше или равно
~= Не равно
== Равно

Если мы напишем "CMaster " == "CM " - у нас будет значение False , то есть, ложь
Если мы напишем "CMaster " == "CMaster " - у нас будет значение True , то есть, истина.

5 > 10 -- ложь 5 < 10 -- истина 10 ~= 15 -- истина 10 >= 5 -- истина

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

Тот код, который мы писали ранее:

Function onScriptStart() printLog("Hello world!"); end

Преобразуем следующим образом:

Function onScriptStart() botName = getNickName() if(botName == "CMaster") then printLog("CM FOREVER"); else printLog("Nonamer"); end end

Давайте разберём этот код начиная сверху. Я советую сразу начинать учить читать код. Поэтому попробуем прочитать, что у нас получилось

Function onScriptStart() - создаём фукнцию с именем onScriptStart botName = getNickName() - в переменную botName записываем имя бота if(botName == "CMaster") then - если имя бота равно "CMaster", значит printLog("CM FOREVER"); - пишем в чат "CM Forever". else- ИНАЧЕ, или же если имя бота НЕ РАВНО "CMaster" printLog("Nonamer"); - пишем в чат "Nonamer" end- конец условий end- конец функции

Давайте попробуем проверить код, который мы написали. Я сохранил измененный код так же под именем "example.lua " и запустил RakBot с ником "Mason_Bennett ".

После загрузки нашего сценария, RakBot написал в чат Nonamer. Попробуем зайти с ником "CMaster ".

Как мы видим, наше условие успешно работает и мы видим в чате то, что и хотели.

Пройдёмся немного по переменным. У Вас есть лист бумаги и Вы хотите его сохранить. Сохранить каким образом - куда-то положить, чтобы не потерять его. Например, мы можем положить наш лист бумаги в шкафчик и достать тогда, когда нам будет необходимо. Если у нас будет новый листок и нам не нужен будет старый - мы выкинем старый листок и положим новый.
Это и есть логика переменной. Мы можем создавать переменную с именами, которыми хотим и записывать в них значения, что мы и сделали в предыдущем примере с переменной botName.

В Lua мы можем записывать в переменную всё, что мы хотим. Например, я хочу создать переменную с именем PaperList и записать в неё текст "Lua - урок №2 ". Для этого я напишу:

PaperList = "Lua - урок №1"

Что мы здесь сделали? Написали имя и использовали оператор присваивания "=" и теперь я могу использовать эту переменную в любом месте своего сценария.
Думаю, что если Вы вспомните математику на уровне максимум 5 класса - тут будет всё понятно.

У Lua есть несколько типов переменных, это nil, boolean, number, string . Не бойтесь, это всё очень просто.

На самом деле их несколько больше, но я уже говорил, что в RakBot большая часть функционала отсутствует.

nil - отсуствие значения.
boolean - логические значения, принимает два варианта значений - либо true, либо false.
number - вещественное число с двойной точностью. В Lua нет целочисленного типа, поэтому он выступает в качестве и вещественного и целочисленного типа.
string - строка, здесь, я думаю, всё понятно.
Чтож, давайте попробуем создать несколько переменных и "поиграться" с ними.

number = 0; - создаём переменную с именем number и присваиваем значение 0
number = number + 5; - присваивание значения переменной number + 5 (то есть, 0 + 5), теперь у нас хранится здесь число 5.
number ++; - ++ - инкремент. Другими словами - вы берёте переменную и увеличиваете её на одну единицу. То есть (5 + 1) - теперь 6 лежит у нас в переменной number.
number --; - -- декремент. Другими словами - уменьшаем на одну единицу. (6 - 1) - теперь значение равно 5.

string = "Hello" - создаём переменную string со значением "Hello"
string = string .. "," - конкатенация строк, оно же сложение строк. Что мы здесь сделали? Указани имя переменной, указали оператор конкатенации ".. ", после чего указали ещё одну строку, которую необходимо добавить к первой. Теперь у нас в переменной "string " лежит значение "Hello,".
string = string .. getNickName () - теперь, к "Hello," мы добавили ник бота, пускай будет "Michel". Теперь у нас в переменной string лежит значение "Hello,Michel".

boolean = true ; - создаём переменную boolean со значением true (ИСТИНА).
boolean = getNickName () == "Dimosha" - сравниваем имя бота со строкой Dimosha. Так как имя бота у нас Michel, из предыдущего примера, сюда запишется значение false (ЛОЖЬ).

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

Давайте пройдёмся по самому простому пути: фукнция без параметров и без возвращаемого значения, которая будет складывать 5 + 10 и выводить результат в консоль RakBot"а.

Я создам функцию с именем Add :

Function Add() -- Создаём фукнцию Add printLog(5 + 10) -- используем метод RakBot для вывода в консоль end-- Конец функции

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

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

Function Add(a, b) printLog(5 + 10) end

Теперь в методе у нас доступны два значения, которые содержатся в двух новых переменных a и b, но на консоль у меня всё равно выводится 15. Исправим это:

Function Add(a, b) printLog(a + b) end

Идеально. Теперь, при вызове этого метода, мы будем получать результат сложения в консоли. Попробуем протестировать. Изменим наш код в example.lua на следующий:

Function Add(a, b) printLog(a + b) end function onScriptStart() Add(5, 10); Add(123, 4324); Add(555, 111); end

И попробуем запустить RakBot. Посмотрим, что из этого получится:

Это решило нашу первую проблему. Попробуем решить вторую, чтобы наша функция возвращала результат.

Перепишем фукнцию Add :

Function Add(a, b) return a + b end

return - ключевое слово для возвращения значения из функции. Перепишем теперь метод onScriptStart :

Function onScriptStart() printLog("Первое значение: "..Add(5, 10)); printLog("Второе значение: "..Add(123, 4324)); printLog("Третье значение: "..Add(555, 111)); end

Посмотрим, что получилось.

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

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

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

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

Функция Lua Существуют две основные цели:

  • 1. выполнить поставленную задачу, в данном случае используется в качестве операторе вызова функции;
  • 2. Вычислить и возвращает значение, в этом случае функция используется в качестве выражения присваивания.

определение функции

функции языка программирования Lua, определенные в следующем формате:

Optional_function_scope function function_name(argument1, argument2, argument3..., argumentn) function_body return result_params_comma_separated end

Разрешение:

  • optional_function_scope
  • Этот параметр является необязательным функция разработать глобальную функцию или локальную функцию, конец параметра не установлен в глобальной функции, если вам необходимо установить функцию является частичной функцией вам необходимо использовать ключевое слово местное.
  • function_name:
  • Укажите имя функции.
  • аргумент1, argument2, argument3 ..., argumentn :
  • Параметры функции, несколько параметров, разделенных запятыми, может также функционировать без параметров.
  • function_body:
  • Тело функции, утверждения функциональных блоков кода должны быть выполнены.
  • result_params_comma_separated:
  • возвращаемые значения функций, функция Lua язык может возвращать несколько значений, разделенных запятыми.

    примеров

    Следующий пример определяет функцию(макс), параметры num1, num2, используемый для сравнения размеров двух значений и возвращает максимальное значение:

--[[ 函数返回两个值的最大值 --]] function max(num1, num2) if (num1 > num2) then result = num1; else result = num2; end return result; end -- 调用函数 print("两值比较最大值为 ",max(10,4)) print("两值比较最大值为 ",max(5,6))

两值比较最大值为 10 两值比较最大值为 6

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

Myprint = function(param) print("这是打印函数 - ##",param,"##") end function add(num1,num2,functionPrint) result = num1 + num2 -- 调用传递的函数参数 functionPrint(result) end myprint(10) -- myprint 函数作为参数传递 add(2,5,myprint)

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

这是打印函数 - ## 10 ## 这是打印函数 - ## 7 ##

Несколько возвращаемых значений

Функция Lua может возвращать несколько значений результата, такие как string.find, который возвращает соответствующий строку "начало и конец подстрочный" (если нет строка матч возвращает не ноль).

> s, e = string.find("www.сайт", "w3big") > print(s, e) 5 10

Функция Lua, после возвращения стоит возвращать список списков может возвращать несколько значений, таких как:

Function maximum (a) local mi = 1 -- 最大值索引 local m = a -- 最大值 for i,val in ipairs(a) do if val > m then mi = i m = val end end return m, mi end print(maximum({8,10,23,12,5}))

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

Переменный параметр

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

Параметры Lua функции в таблице называется Арг, #arg представляет число параметров, передаваемых.

Например, мы вычислить среднее значение нескольких чисел:

Function average(...) result = 0 local arg={...} for i,v in ipairs(arg) do result = result + v end print("总共传入 " .. #arg .. " 个数") return result/#arg end print("平均值为",average(10,5,3,4,5,6))

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

总共传入 6 个数 平均值为 5.5



Рекомендуем почитать

Наверх