С помощью метатаблиц можно реализовать некое подобие объектно-ориентированного программирования.
Попробуем это сделать.
Сначала создадим базовый класс (обычная таблица):
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".
Модули - это один из способов грамотно организовать большой проект в 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
В общем, все очевидно. Неудобств несколько - необходимость внутри файла
модуля (например, в 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.
Путь - это последовательность шаблонов, разделенных точкой с запятой.
В каждый шаблон функция поиска будет подставлять имя модуля, а
затем будет пытаться открыть файл с именем, полученным из
шаблона.
То загрузчик будет пытаться открыть следующие файлы (порядок сохранен):
./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.
Такая организация позволяет упаковывать несколько субмодулей внутри
одной библиотеки, используя для загрузки каждого субмодуля отдельную
функцию. Уф, насилу перевел! Теперь быстрее к самому вкусному - ООП на модулях!
ООП на модулях
В С++ это довольно привычно, располагать каждый класс в отдельном файле
(обычно в двух, ИмяКласса.h и ИмяКласса.cpp). В Lua, поверьте, это тоже
удобно :).
Проект удобно организовать следующим образом: .
..
<luamodules> - папка
<cmodules> - папка
start.lua
...
Пути к Lua-библиотекам и С-библиотекам (загрузчики) можно прописать двумя различными способами.
Первый способ: В первых двух строчках файла start.lua написать:
package.path = './luamodules/?.lua' -- пути к Lua библиотекам package.cpath = './cmodules/?.dll' -- пути к С библиотекам
Второй способ:
Использовать системные переменные.
Создать файл start.bat в котором написать следующее:
set LUA_PATH=.\luamodules\?.lua; set LUA_CPATH=.\cmodules?.dll lua.exe start.lua
А в самом файле start.lua уже ничего не писать.
Итак, повторим наш учебный проект, но теперь с использованием модулей.
Первый модуль назовем Factory. Он будет создавать объекты классов и устанавливать метатаблицы.
Создадим файл Factory.lua (мы еще помним, что при поиске модуля Factory в шаблон package.path будет подставляться имя модуля?). Сохраним его в папке ./luamodules.
В этом файле напишем следующее:
local base = _G
module('Factory')
function setBaseClass(class, baseClass) base.assert(baseClass.mtab) base.setmetatable(class, baseClass.mtab) end
function create(class, ...) local w = {} setBaseClass(w, class) w:construct(base.unpack(arg)) return w end
В начале файла мы видим магическую строку
local base = _G
Зачем она? Функция module,
если вспомнить выше сказанное о ней, во время своего выполнения
переключает локальный контекст. Это означает, что после её вызова, если
ничего не предпринять, доступ к глобальным переменным Lua будет закрыт.
Поэтому мы сохраняем специальную глобальную переменную Lua _G (_G._G = _G) в локальной переменной base. Теперь доступ к глобальным переменным осуществляется через base.имя_переменной (в нашем случае это base.assert, base.setmetatable и base.unpack).
Так, прочный фундамент мы заложили, приступим к надстройке. Создадим файл./luamodules/Base.lua
local base = _G
module('Base') mtab = { __index = _M }
local Factory = base.require('Factory')
function new() return Factory.create(_M) end
function construct(self) base.print('Base created!') self.field = 'text' end
function setField(self, field) -- метод получения значения поля field self.field = field end
function getField(self) -- метод установки значения поля field return self.field end
Проясним некоторые места:
local Factory = base.require('Factory')
Для работы нам будет нужен модуль Factory.
Внимание!
Для избежания ошибок с циклическими зависимостями модулей, все функции require нужно вызывать после выполнения функции module!
mtab = { __index = _M }
Вспоминаем про функцию module - _M - это ссылка на саму таблицу Base.
mtab = { __index = _M }
Таблица mtab будет установлена как метатаблица у нового объекта класса Base при вызове метода create()модуля Factory. Это означает, что при поиске полей внутри объекта класса Base, если поле будет не найдено, то поиск будет осуществляться в таблице, на которую ссылается поле метатаблицы __index (см. Метатаблицы).
function new() return Factory.create(_M) end
Для удобства. Конечно, в прикладном коде можно вызвать и
local base = Factory.create(Base)
но мне кажется что вызов
local base = Base.new()
короче и очевидней.
function construct(self) base.print('Base created!') end
Эта функция будет вызвана из методаcreate() модуля Factory. Фактически, это конструктор класса.
Все методы класса имеют первым параметром ссылку на объект класса self.
Напишем тестовый скрипт ./start.lua:
package.path = './luamodules/?.lua' -- пути к Lua библиотекам package.cpath = './cmodules/?.dll' -- пути к С библиотекам
require('Base')
local base = Base.new() print(base:getField()) base:setField(1) print(base:getField())
local Factory = base.require('Factory') local Base = base.require('Base')
Factory.setBaseClass(_M, Base) -- устанавливаем Base как базовый класс
function new(param1, param2) -- передаем параметры в конструктор return Factory.create(_M, param1, param2) end
function construct(self, param1, param2) -- конструктор с параметрами Base.construct(self) -- вызов конструктора базового класса base.print('Child created!', param1, param2) end
function getField(self) -- переопределяем метод return 'zzz' end
Модернизируем немного ./start.lua, добавив в него строки:
require('Child')
и
local child = Child.new(1, 2) print(child:getField()) child:setField(1) print(child:getField())
Запускаем его на выполнение.
lua.exe start.lua
Должны получить:
Base created! text 1 Base created! Child created! 1 2 zzz zzz
Завершающие штрихи. Как создасть статический член класса? Очень просто. Внутри модуля объявить его либо как
member = 1
либо (предпочтительнее, поскольку более очевидно чего вы хотите)
_M.member = 1
Соответственно, статический метод класса будет без первого параметра self: