Руководство по Lua

Руководство по Lua

Статьи


Методы

Рассмотрим следующую конструкцию (попытка сымитировать ООП):

local t = {x = 1}

function t:fun()
    print(self.x)
end
                     
t:fun()

-- > 1

Что мы здесь сделали?
1. Создали таблицу t;
2. В таблице t завели поле x и назначили ему значение 1;
3. В таблице t завели функцию fun;
4. Внутри функции fun обратились к таблице через специальную переменную self (на самом деле self это первый скрытый параметр у функции fun. Вызывая функцию как t:fun() , мы а самом деле сделали то же самое, что и t.fun(t))
5. Вызвали у таблицы (уже почти объекта!) t метод fun;

На что все это похоже? Это похоже на ООП. Есть, объект, есть метод объекта, есть вызов метода объекта. Чего нет? Нет возможности создавать объекты определенного типа (например типа t). То есть и объект, и метод существуют в единственном экземпляре. Для того, чтобы создать объект типа tt, который ведет себя так же, как объект типа t, нам придется повторить всю процедуру с самого начала:

local tt = {x = 2}

function tt:fun()
    print(self.x)
end
                     
function checkMetod(x)
    x:fun()
end

checkMetod(t)
checkMetod(tt)

--> 1
--> 2

Есть ли выход из сложившейся ситуации? Да, есть. Но о нем мы поговорим после того, как рассмотрим метатаблицы.

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

Метатаблицы позволяют переопределить поведение встроенных типов. Для таблиц это можно сделать прямо в Lua, а для других типов придется это делать через С.
Метатаблицы схожи с конструкцией operator() в C++. Рассмотрим, например, как можно складывать две таблицы (в самом языке такая операция не предусмотрена).

-- первый операнд
local data1 = {x = 1}
-- второй операнд
local data2 = {x = 2}
-- создаем таблицу
local meta = {}
-- в таблице определяем специальное поле __add (оператор +)
function meta.__add(op1, op2)
  return op1.x + op2.x
end
-- устанавливаем метатаблицу первому операнду
setmetatable(data1, meta)
-- проверяем
print(data1 + data2) --> 3

Итак, что мы сделали? Создали два объекта, каждый из которых содержит поле х. Затем создали обычную таблицу meta. В ней определили функцию со специальным именем __add. Затем установили метатаблицу первому операнду.
С тем же успехом мы могли бы установить эту метатаблицу и второму операнду. Когда Lua обнаруживает операцию над значением, то сначала ищется подходящий обработчик в метатаблице первого операнда, и если там нет, то ищется обработчик в метатаблице второго операнда. Если обработчика и там нет, то генерируется ошибка.
Вот список операций, обработчики которых могут быть переопределены в метатаблице:

Обработчик Оператор Lua Примечание
__add(op1, op2) + сложение
__sub(op1, op2) - вычитание
__mul(op1, op2) * умножение
__div(op1, op2) / деление
__mod(op1, op2) % деление по модулю
__pow(op1, op2) ^ возведение в степень
__unm(op) - унарный минус
__concat(op1, op2) .. конкатенация (склейка)
__len(op) # унарный оператор взятия длины
__eq(op1, op2) == оператор "равно"
__lt(op1, op2) < оператор "меньше"
__le(op1, op2) <= оператор "меньше либо равно"
__index(op, key) [] оператор обращения по ключу
вызовется если, например,  вызвать local x = data1[1]
__newindex(op, key, value) [] оператор присвоения нового значения по ключу
если сделать эту функцию пустой, то таблица станет "readonly", то есть в нее нельзя будет добавить новое поле со значением
 вызовется если, например,  вызвать data1[1] = 1
__call(op, ...) () оператор вызова функции
вызовется если, например,  вызвать data1(1, 2, 3)
в обработчик __call первым параметром придет операнд (в нашем случае data1), а следующими параметрами будут те параметры, что стояли в скобках (в нашем случае 1, 2, 3).


Как видно, все очень просто.

ООП

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

Base = {}

В ней создадим поле field:

Base.field = "text"

и две функции для работы с этим полем:

function Base:setField(value)
    self.field = value
end

function Base:getField()
    return self.field
end

Обратите внимание на двоеточие перед именем метода - первым параметром в метод будет передаваться self, то есть сам объект класса, по аналогии с С++ это this, только в С++ this передается в метод неявно.
Итак, базовый класс создан. Это шаблон, по которому должны создаваться объекты данного типа.
Создадим объект типа Base:

local base = {}

Как видно, пока это обычная таблица. Установим у этой таблицы новую метатаблицу.

setmetatable(base, {__index = Base})

Вуаля! Теперь самая обычная таблица стала объектом типа Base. Что это значит? Согласно правилам, описанным в предыдущем пункте Метатаблицы, оператор __index в метатаблице вызывается всякий раз, когда осуществляется поиск в таблице по ключу. А что есть вызовы "методов" и обращение к "членам класса", как не поиск поля в таблице?
Сначала ищутся поля в самой таблице. Если поле не найдено,  в метатаблице ищется метод __index. Если __index является функцией, вызывается эта функция. Если __index является таблицей, то поиск поля осуществляется в этой таблице. Эти вызовы делаются рекурсивно до тех пор, пока метатаблица либо __index не станет равным nil. С учетом вышесказанного становится понятным, почему следующий вызов дает такой результат:

print(base:getField()) --> text

Сначала был произведен поиск поля  getField в таблице base. Ничего не найдено. Далее в таблице base ищется метатаблица. Найдено. В ней ищется поле __index. Найдено. Поле __index является таблицей. В таблице __index (а это на самом деле ссылка на таблицу Base) ищется поле  getField. Найдено. Это функция. Поскольку мы вызывали base:getField(), то в функцию getField первым параметром передается таблица base. Далее внутри функции getField повторяется аналогичный поиск для поля field. В итоге печатается первоначально заданное значение этого поля "text".

Изменим значение этого поля:

base:setField('aaa')
print(base:getField()) --> aaa

Теперь мы хотим наследоваться от класса Base. Наследование выполняется аналогично созданию объекта класса Base:

Child = {}
setmetatable(Child, {__index = Base})

Переопределим в Child метод getField(), фактически, делаем этот метод виртуальным:

function Child:getField()
    return 'zzz'
end

Создадим объект типа Child:

local child = {}
setmetatable(child, {__index = Child})
print(child:getField()) --> zzz

А как же быть, если мы хотим вызвать для Child метод родительского класса? А очень просто:

child:setField('ooo')
print(Base.getField(child)) --> ooo


Модули

Модули - это один из способов грамотно организовать большой проект в Lua. Это что-то типа namespace в С++.
Для начала рассмотрим наивную попытку организовать модули самостоятельно. Сделать это нетрудно. Достаточно создать файл, где будут созданы глобальные таблицы с именами модулей, и далее в программе обращаться к этим глобальным таблицам.
Например:

файл modules.lua:

Module1 = {}
Module2 = {}

файл module1.lua:

Module1.var1 = 1
function Module1.fun1(a, b)
    return a + b + Module1.var1
end

аналогично файл module2.lua:

Module2.var1 = 2
function Module2.fun1(a, b)
    return a + b - Module2.var1
end

файл запуска приложения main.lua:

dofile('modules.lua')
dofile('module1.lua')
dofile('module2.lua')

print(Module1.fun1(1, 2)) --> 4
print(Module2.fun1(1, 2)) --> 1

В общем, все очевидно. Неудобств несколько - необходимость внутри файла модуля (например, в module1.lua) при обращении к переменным модуля все время ссылаться на глобальную таблицу с именем модуля (Module1.var1), вероятность засорения глобального пространства имен именами временных переменных (написав, например, вместо local x = 1 просто x = 1), ну и еще есть какие-нибудь недостатки, включая главный - рукотворность этой
схемы.

Теперь рассмотрим, что нам предлагает Lua. Модули создаются глобальной функцией module(name). Эта функция проверяет, существует ли глобальная таблица с именем name. Если нет, то такая таблица создается. Затем эта таблица помещается в глобальную таблицу package.loaded[name] = table. Функция module(name) также устанавливает в созданной таблице поля t._NAME = name, t._M = t и t._PACKAGE = полное имя модуля минус последний компонент (если имя модуля составное). Имя модуля может состоять из нескольких частей, разделенных точками. В этом случае функция module создает (или использует, если уже существуют) таблицы для каждого компонента. Например вызов  module(a.b.c) создаст глобальную таблицу a, в ней таблицу b, а в таблице b создаст таблицу с. Кроме того, функция module делает еще одну важную вещь - устанавливает созданную таблицу как новый environment (окружение). Это значит, что все последующие обращения к глобальным переменным (до конца блока, обычно до конца текущего файла) будут направляться в новую таблицу.

Загрузка модулей

Чтобы модулем можно было пользоваться, его надо загрузить. Для этого нужно вызвать глобальную функцию require(name). Эта функция начинает с просмотра таблицы package.loaded, чтобы определить, модуль name  уже загружен или еще нет. Если модуль name загружен, то require возвращает значение package.loaded[name]. Если же нет, то require ищет загрузчик модуля.
<Далее идет почти буквальный перевод мануала. Переводил тщательно, чтобы самому все понять :)>
Загрузчики модулей хранятся в таблице package.loaders. Каждое поле этой таблицы является функцией поиска. Когда функция require ищет модуль, то она вызывает эти функции по очереди. Функция поиска может вернуть либо другую функцию (загрузчик модуля), либо строку, которая разъясняет, почему модуль не был найден (либо nil, если ей нечего сказать). Lua инициализирует таблицу package.loaders четырьмя функциями.

Первая функция ищет загрузчик в таблице package.preload.

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

package.path = "./?.lua;./?.lc;/usr/local/?/init.lua"

то для загрузки модуля foo Lua будет пытаться открыть следующие файлы (порядок поиска совпадает с порядком задания шаблонов):

./foo.lua, ./foo.lc, and /usr/local/foo/init.lua.

Третья функция ищет загрузчик С-библиотек, используя пути, прописанные в package.cpath. Например, если

package.cpath = "./?.so;./?.dll;/usr/local/?/init.so"

то загрузчик будет пытаться открыть следующие файлы (порядок сохранен):

 ./foo.so, ./foo.dll, and /usr/local/foo/init.so.

Первая найденная библиотека динамически линкуется с программой. Затем загрузчик пытается найти С функцию внутри библиотеки, чтобы использовать ее для загрузки. Имя этой С-функции должно быть "luaopen_" + копия имени модуля, причем в имени модуля все точки должны быть заменены на подчеркивания. Кроме того, если в имени модуля есть дефис, то весь префикс, включая знак дефис, должен быть удален. Например, если имя модуля a.v1-b.c, то имя функции должно быть luaopen_b_c.

Четвертая функция пытается использовать загрузчик "все в одном". Эта функция использует шаблоны для загрузки С-библиотек, чтобы найти библиотеку для корневого модуля. Например, для модуля a.b.c эта функция будет искать С-библиотеку модуля a. Если такая библиотека найдена, то внутри нее ищется функция для загрузки субмодулей. В нашем примере такая функция должна иметь имя luaopen_a_b_c. Такая организация позволяет упаковывать несколько субмодулей внутри одной библиотеки, используя для загрузки каждого субмодуля отдельную функцию.
Уф, насилу перевел!
Теперь быстрее к самому вкусному - ООП на модулях!


Работы в порту (грузчик и развозчик)
Работы в порту (грузчик и развозчик)
5-02-2021, 12:00, Скрипты
Смена производителя шин
Смена производителя шин
7-12-2020, 21:22, Скрипты
Cистема уровней
Cистема уровней
22-12-2020, 12:00, Скрипты
Телепорт-панель на DGS
Телепорт-панель на DGS
6-12-2020, 19:16, Скрипты
Movie
В данной публикации отсутствуют комментарии !

Перед публикацией, советую ознакомится с правилами!