Гайд Метатаблицы, метаметоды, переменная self [продвинутая Lua]

Интересно? Продолжать подобные гайды, привести реальные примеры использования?


  • Всего проголосовало
    77

date

Новичок
Автор темы
2
67
Приветы!

Я начал скриптовать на луа примерно 10 лет назад (9 с копейками, но для красоты округлим).
В данном посте решил затронуть несколько продвинутых тем по луа, которые не так очевидны, и возможно неизвестны даже для "уверенных луа скриптеров".

Погнали!

FYP написал(а):
Нихуя вы не знаете, но я вас научу!


Для начала, давайте возьмем за тезис - луа основана на таблицах. Не верите? Ну, ладно, давайте не будем брать за тесис, а проверим.

Все глобальные таблицы, функции, переменные - все это попадает в одну общую таблицу _G.
Локальные вещи (те, что local) - они работаю чуть иначе, и работать с ними можно только через библиотеку debug (не нужно!).

Набросал маленький быдлокод, который печатает содержимое таблицы _G:

Lua:
local printed = {};

local function has_value (tab, val)
    for index, value in ipairs(tab) do
        if value == val then
            return true
        end
    end

    return false
end

function printTable(t, indent)
    for k, v in pairs(t) do
        if not has_value(printed, v) then
            if type(v) == "table" then
                table.insert(printed, v);
                print(string.rep(".", indent * 4) .. "[" .. k .. "] (T:" .. type(v) .. ") => {")
                printTable(v, indent + 1);
                print(string.rep(".", indent * 4) .. "}")
            else
                print(string.rep(".", indent * 4) .. "[" .. k .. "] (T:" .. type(v) .. ") => " .. tostring(v))
            end
        end
    end
end


print("*************** _G table structure: ***************")
printTable(_G, 0);
print("*************** ok we done :)) ***************")

Запустить можно в Муне, либо через командную строку исполнить lua файл.
Третий вариант: dofile("путь") в LUA CLI.

Окей, таблица _G, поняли, дальше-то что? Ну, вообще удобно, чтобы получать доступ к глобальным элементам через строкове имя, например:

Lua:
local toStr = "tostring";
print(_G[toStr]("HAHA"));

Дальше поговорим о мета таблицах...

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

Lua:
-- В данном случае создаем "класс" cool
local cool = {};

-- Внутри класса cool создаем новую таблицу mt, которая будет являться метатаблицей.
cool.mt = {};

-- создаем внутри таблицы mt метод с именем __add, который переопределяет поведение операции сложения (+)
function cool.mt.__add(a, b)
    return (a.somefield + b.somefield) * 2;
end

-- cool.new() -> метод конструктор нашего крутого "класса" cool.
function cool.new(somefieldValue)
    -- Создаем новую таблицу
    local newCool = {};

    -- Задаем полю somefield аргумент который прислали в функцию.
    newCool.somefield = somefieldValue;

    -- Задаем вновьсозданной таблице метатаблицу cool.mt
    setmetatable(newCool, cool.mt);
   
    -- Возвращаем вновьсозданную таблицу.
    return newCool;
end

-- Создаем 2 инстанции нашего "крутого" класса передавая параметром в конструктор разные аргументы.
local intanceOfCoolClass = cool.new(2);
local secondIntanceOfCoolClass = cool.new(3);

-- проверим, что конструктор сработал:
print("---- Class values: ----");
print("Class 1: " .. intanceOfCoolClass.somefield);
print("Class 2: " .. secondIntanceOfCoolClass.somefield);
print("Class 1 metatable: " .. tostring(getmetatable(intanceOfCoolClass)));
print("Class 2 metatable: " .. tostring(getmetatable(intanceOfCoolClass)));
print("WOW TABLES EQUALS!!11");
print("---- End of test stuff ----");
-- Как видим - значения разные, метатаблица одна (получать можно через getmetatable)


-- Терь проверим перегрузку сложение (метода +)
-- У нас там (somefield + somefield) * 2, т.е. должно получится (2 + 3) * 2 => 10.
print("Result: " .. intanceOfCoolClass + secondIntanceOfCoolClass);

-- Ого, мы крутые!

Собственно, из кода, думаю, все понятно. Если нет - пишите вопросы, но если вы не знаете луа, то все описанное в этой статье вам не нужно. А если знаете - должны понять по коду.
Данные поля, к слову, именуются метаметодами.

Список других зарезервированных метаметодов:
Код:
__add -> +
__sub -> -
__mul -> *
__div -> /
__unm -> ~ или -
__pow -> ^
__concat -> ..
_eq -> ==
__lt -> <
__le -> <=
__tostring -> метаметод, который вызывается функцией tostring()

При любой из этих операций сначала проверяется наличие перегруженного оператора у первого объекта, потом у второго, если нету - ошибка.
Первый это тот, который написан первым a + b — тут a первый >< ))

Отдельно хочу заметить метаметод __index, потому что он особенно интересен.
Реализация выглядит вот так:

Lua:
cool.mt.__index = function (table, key)
   return "Key " .. key .. " not found!";
end

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

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

Lua:
local coolDefaultValues = {
   somefield2 = -1337
}

cool.mt.__index = coolDefaultValues;

print(cool.new(123).somefield2)

Пример выше разумеется нужно совместить с прошлым.
Удобно применять для реализации "базовых" классов (таблиц, объектов),

Так же существует метаметод __newindex, вызывается при записи нового индекса в таблицу (объект, ага, уже сам не знаю как их называть в итоге).

Выглядит следующим образом:

Lua:
cool.mt.__newindex = function (t,k,v)
{
   print("Table: " .. t .. " | Key: " .. k .. " | Value: " .. tostring(v));
}


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

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

В конце немного про вызов методов через ":".

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

Lua:
local myTable = {}

myTable.bhack = "1337";

myTable:SomeMethod()
{
   -- self == myTable;
   print(self.bhack);
}

myTable:SomeMethod();

Т.е. вызвая функцию через ":" в нее автоматический передается параметр self, который содержит в себе таблицу, содержащую вызываемый метод.

Параметр self можно записывать явно в аргументах метода, а можно и не записывать. Пример выше мог выглядеть вот так:

Lua:
local myTable = {}

myTable.bhack = "1337";

myTable:SomeMethod(self)
{
   -- self == myTable;
   print(self.bhack);
}

myTable:SomeMethod();

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

Lua:
local myTable = {}

myTable.bhack = "1337";

myTable:SomeMethod(self)
{
   -- self == myTable;
   print(self.bhack);
}

myTable.SomeMethod(myTable);


Небольшой общий пример:


Lua:
local meCool = {};

meCool.DB = {};

function printTable(t, indent)
    for k, v in pairs(t) do
            if type(v) == "table" then
                table.insert(printed, v);
                print(string.rep(".", indent * 4) .. "[" .. k .. "] (T:" .. type(v) .. ") => {")
                printTable(v, indent + 1);
                print(string.rep(".", indent * 4) .. "}")
            else
                print(string.rep(".", indent * 4) .. "[" .. k .. "] (T:" .. type(v) .. ") => " .. tostring(v))
            end
    end
end

function meCool.DB:SomeMethod()
   
    print("***** content of self: *****");
    printTable(self, 0);
    print("***** end of self: *****");

    self.SomeMethod2();
end

function meCool.DB:SomeMethod2()
    print("ya i am here!");
end

function meCool.DB.SomeMethod3(self, exampleVar)
    print("***** content of self: *****");
    printTable(self, 0);
    print("***** end of self: *****");

    print("Btw var is: " .. exampleVar);
end

meCool.DB:SomeMethod();
meCool.DB:SomeMethod3("woah!");
meCool.DB.SomeMethod3(meCool.DB, "woah2!");


На этом пока все, прошу прощения за сумбурность изложения, возможно в скором времени причешу данный пост, писал урывками на работе, ушло аж 3 дня по 10 минут %)
Больше времени пока нету.
Пишите вопросы, отвечу!

UPD: поправил табуляцию
 
Последнее редактирование:

ufdhbi

Известный
Проверенный
1,455
861
Было интересно почитать, но пользовался этим ранее, удобно
 

date

Новичок
Автор темы
2
67
Вот бы все новореги были такими. Поучительно и интересно, будет время - продолжай.
Да я не то, чтобы совсем новорег. Пару лет здесь ошиваюсь, просто не особо интересуюсь читами, скорее интересен сам факт модификации и развития сампа. До этого не было причин и мотивации создавать аккаунт, а здесь в очередной раз зашел (делаю это раз в год :) ) и увидел, что завезли Lua.

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

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

Akionka

akionka.lua
Проверенный
742
499
Круто, но пока не очень лично для меня понятно где это можно применить. Жду ещё уроки.
upd 22.06.19: не дождался:(
upd 21.08.19: :(
upd 09.03.20: надежда умирает последней
upd 29.10.23: надежда умерла. но на форуме вышли другие статьи про метатаблицы
 
Последнее редактирование: