Lua. Краткое введение в метатаблицы для чайников
На написание данной статьи меня сподвигло большое количество вопросов по метатаблицам и ООП в Lua, благо это самый сложный и проблематичный раздел у изучающих данный язык, но, так как Lua проектировалась как язык для начинающих и не-программистов и, в целом, имеет небольшой объём материала для освоения, негоже оставлять её «на потом», учитывая что с помощью метатаблиц можно творить чудеса и крайне элегантные решения заковыристых задач.
В данной публикации будет описание всех стандартных мета-методов для версий Lua 5.1–5.3, с примерами. Начнём.
Метатаблицы
Что это такое?
На самом деле, метатаблица ничем не отличается от обычной таблицы, за исключением того что она указана как управляющая.
Схематично можно представить, например, так:
В метатаблице описывается реакция основной таблицы на воздействия, например, вызов таблицы как функцию, деление таблицы на произвольное значение, или попытка вытянуть из неё ключ, которого у неё нет, благодаря специальным ключам (Lua 5.1, если не указано обратное):
Общие метаметоды
- __index — функция или таблица, с помощью которых оригинальная таблица ищет ключи, если их не существует;
- __newindex — функция, как добавлять в таблицу *новые* ключи, на уже существующие — не действует;
- __call — фунция, которая вызовется когда таблицу попробуют вызвать как как функцию;
- __tostring — функция, вызывающаяся при попытке преобразовать таблицу в строку, например, при print или tostring, сочетается с __concat;
- __concat — функция, вызывающаяся при попытке конкатенации таблицы с чем либо, сочетается с __tostring;
- __metatable — значение, которое возвращается попытке взять метатаблицу у данной таблицы, позволяет скрывать метатаблицы;
- __mode — строка, управляющая «силой» связей в таблице при сборке мусора, с её помощью можно создавать таблицы слабых ссылок или эфемероны;
- __gc — функция, которая будет вызвана при сборе userdata (5.1+) или таблицы (5.2+) мусорщиком, если очень хочется в 5.1 — есть способ применения;
- __len — функция которая будет вызываться при попытке вычисления длины таблицы, с помощью оператора # (5.2+);
- __pairs — функция, альтернатива итератора pairs для данной таблицы (5.2+);
- __ipairs — функция, альтернатива ipairs (5.2+);
Математические метаметоды и сравнение (функции)
- __add — (+) сложение;
- __sub — (-) вычитание;
- __mul — (*) умножение;
- __div — (/) деление;
- __pow — (^) возведение в степень;
- __mod — (%) деление по модулю;
- __idiv — (//) деление с изъятием целой части (5.3+);
- __eq — (==) сравнение равенства;
- __lt — (<) сравнение «меньше чем», в обратную сторону выполняется автоматически, реверсируя аргументы;
- __le — (<=) сравнение «меньше или равно»;
Битовые операции (функции, только 5.3+)
- __band — (&) «И»;
- __bor — (|) «ИЛИ»;
- __bxor — (~) исключающее «ИЛИ» (a ~ b);
- __bnot — (~) «НЕТ» (~a);
- __bshl — (<<) битовый сдвиг влево;
- __bshr — (>>) битовый сдвиг вправо.
Примеры
Index
Один из самых распространённых метаметодов, и вызывающий наибольшее число вопросов.
Может быть таблицей или функцией, с аргументами (self, key), где self — таблица для поиска, а key — ключ, значение которого мы хотим получить.
На основе этого метаметода строится большое количество фич, таких как ООП, прокси-таблицы, дефолтные значения таблиц, и ещё много чего.
Иногда может быть вреден, когда необходимо получить точный ключ ВОТ ЭТОЙ таблицы, в таких случаях используют функцию value = rawget (table, key), которая является функцией доступа у таблиц по умолчанию (она, в байткоде, вызывается при попытке получения значения по ключу).
--[[1]]
foo = {}
foo.key = 'value'
--[[2]]
mt = {__index = foo}
-- setmetatable возвращает первый переданный в неё аргумент
--[[3]]
bar = setmetatable({}, mt)
bar.key2 = 'value2'
-- Тестирование:
--[[4]]
print('bar.key2', bar.key2) --> 'value2'
--[[5]]
print('bar.key', bar.key) --> 'value'
--[[6]]
bar.key = 'foobar'
foo.foo = 'snafu'
print('bar.key', bar.key) --> 'foobar'
print('bar.foo', bar.foo) --> 'snafu'
print('bar.foobarsnafu', bar.foobarsnafu) --> nil
--[[7]]
foo = {key = 'FooBar'}
-- так тоже можно
bar = setmetatable({}, {__index = foo})
snafu = setmetatable({}, {__index = bar})
print('snafu.key', snafu.key) --> 'FooBar'
--[[8]]
foo = {}
foo.__index = foo
setmetatable(foo, foo)
-- print('foo.key', foo.key) --> error: loop in gettable
print('foo.__index', foo.__index) --> "table: 0x12345678"
print('rawget(foo, "key")', rawget(foo, "key")) --> nil
--[[9]]
foo = {}
foo.key = 'value'
setmetatable(foo, {__index = function(self, key) return key end})
print('foo.key', foo.key) --> 'value'
print('foo.value', foo.value) --> 'value'
print('foo.snafu', foo.snafu) --> 'snafu'
--[[10]]
fibs = { 1, 1 }
setmetatable(fibs, {
__index = function(self, v)
self[v] = self[v - 1] + self[v - 2]
return self[v]
end
})
Что тут происходит:
1. foo — таблица, в которой мы будем искать ключи, которых у нас нет.
2. mt — таблица, с ключом __index = foo. Если её прицепить к чему-то как метатаблицу, она будет указывать: «Если у нет ключей — попробуйте найти их в foo».
3. Тут — процесс цепляния метатаблицы mt к пустой таблице (которой становится bar)
4. Пример прямого доступа к ключам таблицы. В данном случае, мы берём ключ как обычно, из таблицы bar.
5. Пример доступа к ключам по __index. В данном случае, в таблице bar отсутствует ключ [«key»], и мы ищем его по __index метатаблицы — в таблице foo.
6. Уточнение: если мы внесём в таблицу bar ключ key, он найдётся в ней и при попытке забрать значение — не будет вызвана цепочка метаметодов. Но все остальные несуществующие ключи, такие как [«foo»], будут продолжать вызывать цепочку метаметодов. Ключ [«foobarsnafu»] отсутствует в обеих таблицах, и его значение — закономерный nil.
7. Index позволяет создавать цепочки поиска. В данном случае, алгоритм поиска ключа следующий:
1. Ищем ключ [«key»] в таблице snafu.
2. Не нашли. У метатаблицы snafu есть ключ __index, указывающий на таблицу bar. Ищем там.
3. Опять не нашли. Но и там есть метатаблица с ключом __index, указывающей на таблицу foo. Ищем.
4. Нашли! Вот он наш ключ, и его значение — «FooBar»!
8. В данном случае — мы создаём таблицу с ключом __index, равном ей самой, и устанавливаем её как метатаблицу для самой себя. При попытке получения значения по любому отсутствующему ключу, возникает рекурсивный цикл попыток поиска внутри себя самой, и переходов по __index метатаблицы и дальнейшего поиска. Поэтому, лучше не делать замкнутые цепочки поиска. Если использовать rawget — ни один метаметод не вызывается, и мы получаем точное значение данного ключа.
9. В качестве ключа __index у метатаблицы может быть функция с двумя аргументами — self — сама таблица, key — ключ, значение которого мы хотим получить. Возвращаемое значение функции становится значением. С помощью этого, можно создавать произвольную индексацию у таблиц, или создавать прокси.
10. Пример взят с википедии. В данном случае, __index у таблицы fibs автоматически пересчитывает значения чисел фибоначчи с мемоизацией, т.е. print (fibs[10]) выведет десятое число фибоначчи. Работает через рекурсивное вычисление отсутствующих значений таблицы. Последующие значения мемоизируются в таблицу. Нужно немного времени чтобы понять: если fibs[v — 1] отсутствует — для него выполняется тот же набор действий что и для fibs[v].
NewIndex
Не настолько распространённый метаметод, но тоже иногда удобный для создания закрытых таблиц, фильтрации или проесирования, и ещё нескольких вещей.
Всегда может быть только функцией, с аргументами (self, key, value).
Иногда может быть вреден, поэтому для принудительного не-использования данного метаметода используется функция rawset (self, key, value), который является функцией для таблиц по умолчанию.
--[[1]]
foo = {}
mt = {}
function mt.__newindex(self, key, value)
foo[key] = value
end
bar = setmetatable({a = 10}, mt)
bar.key = 'value'
print('bar.key', bar.key) --> nil
print('foo.key', foo.key) --> 'value'
--[[2]]
bar.a = 20
print('bar.a', bar.a) --> 20
--[[3]]
mt = {}
function mt.__newindex(self, key, value)
if type(value) == 'number' then
-- чтобы не уйти в бесконечный цикл __newindex
rawset(self, key, value)
end
end
foo = setmetatable({}, mt)
foo.key = 'value'
foo.key2 = 100500
print('foo.key', foo.key) --> nil
print('foo.key2', foo.key2) --> 100500
1. Это — простейший пример добавления ключей через прокс-таблицу с помощью метаметода __newindex. Все новые ключи-значения, которые мы добавляем в таблицу bar, добавляются в foo в соответствии с функцией. Self, в данном случае — таблица bar;
2. __newindex распространяется только на несуществующие ключи;
3. Пример функции-фильтра, которая позволяет добавлять в таблицу только числовые значения. Точно так же можно проверять «добавляем только числовые ключи», или заранее создаём несколько таблиц для чисел-строк-таблиц и т.п, и добавляем значения в соответствующие (классификация/балансировка и т.п.).
Call
Данный метаметод удобен для сокращения элементов или вызова дефолтных методов функций с таблицами и для чуть более комфортного ООП, когда мы вызываем таблицу-класс как функцию, и получаем объект.
--[[1]]
mt = {}
function mt.__call(self, a, b, c, d)
return a..b..c..d
end
foo = setmetatable({}, mt)
foo.key = 'value'
print(foo(10, 20, 30, '!')) --> 102030!
print(foo.key) --> 'value'
print(foo.bar) --> nil
--[[2]]
mt = {}
-- Многоточие - все аргументы, которые были переданы в функцию
-- например: a, b, c, d = ...
function mt.__call(self, ...)
return self.default(...)
end
foo = setmetatable({}, mt)
function foo.mul2(a, b)
return a * b
end
function foo.mul3(a, b, c)
return a * b * c
end
foo.default = foo.mul2
print('foo.mul2(2, 3)', foo.mul2(2, 3)) --> 6
print('foo.default(2, 3)', foo.default(2, 3)) --> 6
print('foo.mul3(2, 3, 4)', foo.mul3(2, 3, 4)) --> 24
-- Вызов значения по умолчанию.
print('foo(2, 3)', foo(2, 3)) --> 6
foo.default = foo.mul3
print('Default was changed')
print('foo(2, 3, 4)', foo(2, 3, 4)) --> 24
1. Пример использования метатаблицы, таблицу можно вызывать как функцию. В качестве self передаётся сама таблица, вызванная как функция.
2. В данном примере, мы заполняем таблицу функциями, а метатаблица указывает, что если её вызовут как функцию — выдать результат фунции под ключом default.
Tostring и Concat
Просто приведение объекта к строке и конкатенация.
mt = {}
function mt.__tostring(self)
return '['..table.concat(self, ', ')..']'
end
foo = setmetatable({}, mt)
foo[1] = 10
foo[2] = 20
foo[3] = 30
print('foo', foo) --> [10, 20, 30]
-- print('foo..foo', foo..foo)
-- Ошибка! Невозможно конкатенировать таблицу!
function mt.__concat(a, b)
return tostring(a)..tostring(b)
end
print('foo.."!"', foo.."!") --> [10, 20, 30]!
print('"!"..foo', "!"..foo) --> ![10, 20, 30]
print('foo..foo', foo..foo) --> [10, 20, 30][10, 20, 30]
Metatable
Скрытие метатаблиц, иногда бывает полезно.
mt = {}
mt.id = 12345
foo = setmetatable({}, mt)
print(getmetatable(foo).id) --> 12345
mt.__metatable = 'No metatables here!'
print(getmetatable(foo)) --> 'No metatables here!'
mt.__metatable = false
print(getmetatable(foo)) --> false
Mode
Строка, указывает режим связей значений таблиц.
Если она содержит букву 'k', то слабыми будут объявлены ключи,
если содержит букву 'v' — слабыми станут значения.
Можно использовать их вместе.
В примерах будет использоваться функция collectgarbage — принудительный сбор всего мусора.
Таблицы в Lua — всегда передаются по ссылке.
--[[1]]
mt = {__mode = 'v'}
foo = setmetatable({}, mt)
--[[2]]
bar = {foobar = 'fu'}
foo.key = bar
foo.key2 = {barfoo = 'uf'}
foo[1] = 100500
--[[3]]
print('foo.key.foobar', foo.key.foobar) --> 'fu'
print('foo.key2.barfoo', foo.key2.barfoo) --> 'uf'
print('foo[1]', foo[1]) --> 100500
collectgarbage()
print('foo.key.foobar', foo.key.foobar) --> 'fu'
print('foo[1]', foo[1]) --> 100500
-- print('foo.key2.barfoo', foo.key2.barfoo)
--> Ошибка, key2 не существует!
--[[4]]
bar = nil
collectgarbage()
-- print('foo.key.foobar', foo.key.foobar)
--> Ошибка, key не существует!
1. Пример таблицы слабых значений: если нет ссылок на значения, кроме как в этой таблице — они удалятся в процессе сборки мусора.
2. После исполнения данного участка кода, на таблицу
»{foobar = 'fu'}» существуют две ссылки (в глобальном пространстве и в таблице foo), а на таблицу
»{barfoo = 'uf'}» — одна, внутри foo.
3. Мы видим, что пока в таблице foo есть все значения, но после сборки мусора — исчезает таблица key2. Это происходит потому, что на неё не осталось больше сильных ссылок, только слабые, которые позволяют мусорщику её собрать. К foo[1] это не относится, так как 100500 — не ссылочный тип (не таблица, не функция, не userdata и т.п, а число).
4. Если мы удалим единственную сильную ссылку bar — таблица {foobar = 'fu'} тоже будет уничтожена после сборки мусора.
Аналогичным образом работает с 'k', только по отношению к ссылочным ключам таблицы (foo[{key = 'value'}] = true).
GC
Функция __gc вызовется в том случае, если таблица будет собрана сборщиком мусора. Может использоваться как финалайзер. Функционирует с таблицами и cdata/userdata.
mt = {}
function mt.__gc(self)
print('Table '..tostring(self)..' has been destroyed!')
end
-- lua 5.2+
foo = {}
setmetatable(foo, mt)
-- Lua 5.1
if _VERSION == 'Lua 5.1' then
-- Метаметод __gc работает в 5.1 только по отношению к cdata-типам.
-- Данная методика - грязный хак, но иногда полезен.
-- мы будем удалять ссылку 'foo', тут локальная копия
local t = foo
-- newproxy возвращает cdata-указатель, недокументированная функция Lua 5.1.
local proxy = newproxy(true)
-- метаметод __gc данной cdata - вызов __gc-метаметода таблицы foo
getmetatable(proxy).__gc = function(self) mt.__gc(t) end
foo[proxy] = true
end
print(foo)
foo = nil
collectgarbage()
--> 'Table 0x12345678 has been destroyed!'
Len
Функция, переопределяющая алгоритм вычисления длины таблицы (Lua 5.2+).
mt = {}
function mt.__len(self)
local keys = 0
for k, v in pairs(self) do
keys = keys + 1
end
return keys
end
foo = setmetatable({}, mt)
foo[1] = 10
foo[2] = 20
foo.key = 'value'
print('#foo', #foo) --> 3 (2 в Lua 5.1)
Pairs и Ipairs
Переопределение стандартных итераторов таблиц, для данной таблицы (Lua 5.2+).
mt = {}
function mt.__pairs(self)
local key, value = next(self)
return function()
key, value = next(self, key)
-- Отфильтовываем не-числовые ключи.
while key and type(key) == 'number' do
key, value = next(self, key)
end
return key, value
end
end
function mt.__ipairs(self)
local i = 0
-- ipairs по отрицательным числам.
return function()
i = i - 1
return self[i] and i, self[i]
end
end
foo = setmetatable({}, mt)
foo[1] = 10
foo[2] = 20
foo[-1] = -10
foo[-2] = -20
foo.foo = 'foobar'
foo.bar = 'barfoo'
-- Lua 5.1 пройдёт по всем ключам,
-- 5.2+ - только по строковым
for k, v in pairs(foo) do
print('pairs test', k, v)
end
--> foo foobar
--> bar barfoo
-- Lua 5.1 пройдёт по всем положительным числовым ключам,
-- 5.2+ - только по отрицательным числовым
for i, v in ipairs(foo) do
print('ipairs test', i, v)
end
--> -1 -10
--> -2 -20
Перегрузка операторов
Перегрузка всех операторов работает по одной схеме, детальные примеры для каждого — не нужны.
]]
--[[1]]
vector_mt = {}
function vector_mt.__add(a, b)
local v = {}
v.x = a.x + b.x
v.y = a.y + b.y
return setmetatable(v, vector_mt)
end
function vector_mt.__div(a, b)
local v = {}
v.x = a.x / b.x
v.y = a.y / b.y
return setmetatable(v, vector_mt)
end
-- Для удобства
function vector_mt.__tostring(self)
return '['..self.x..', '..self.y..']'
end
vec1 = setmetatable({}, vector_mt)
vec1.x = 1
vec1.y = 2
vec2 = setmetatable({}, vector_mt)
vec2.x = 3
vec2.y = 4
vec3 = vec1 + vec2
print('vec3', vec3) --> [4, 6]
print('vec2 / vec1', vec2 / vec1) --> [3, 2]
--[[2]]
mt = {}
function mt.__add(a, b)
local insert_position = 1
if type(a) == 'table' and getmetatable(a) == mt then
insert_position = #a + 1
else
a, b = b, a
end
table.insert(a, insert_position, b)
return a
end
-- Для удобства
function mt.__tostring(self)
return '['..table.concat(self, ', ')..']'
end
foo = setmetatable({}, mt)
--[[3]]
foo = 3 + 4 + foo + 10 + 20 + 'a' + 'b'
print('foo', foo) --> [7, 10, 20, a, b]
foo = '12345' + foo
print('foo', foo) --> [12345, 7, 10, 20, a, b]
1. Пример перегрузки операторов на таблицах, которые ведут себя как векторы, благодаря метатаблице. Следует следить за порядком аргументов, каждая операция — возвращает новую таблицу-«вектор».
2. Таблица, в которую можно добавлять элементы оператором »+».
Порядок добавления определяет, в конец или в начало мы добавляем элемент.
3. 3 + 4 выполнится первым, поэтому первый элемент — »7».
В остальных случаях — к результату выполнения предыдущего элемента будет прибавляться следующий:
((7 + foo → foo) + 10 → foo)…
Что со всем этим можно сделать
ООП
Первое что напрашивается — попытка сделать ООП.
Попробуем написать простую функцию, реализующую некоторый абстрактный «класс»:
function Class()
local class = {}
-- Метатаблица для класса.
local mClass = {__index = class}
-- Функция, которая возвращает "объект" данного класса.
function class.instance()
return setmetatable({}, mClass)
end
return class
end
-- Пока без двоеточий.
Rectangle = Class()
function Rectangle.new(x, y, w, h)
local self = Rectangle.instance()
self.x = x or 0
self.y = y or 0
self.w = w or 10
self.h = h or 10
return self
end
function Rectangle.area(self)
return self.w * self.h
end
-- Инстанцируем
rect = Rectangle.new(10, 20, 30, 40)
print('rect.area(rect)', rect.area(rect)) --> 1200
print('rect:area()', rect:area()) --> 1200
Вот, уже что-то похожее на ООП. Тут нет наследования и всяких крутых штук, но это уже неплохо.
При вызове rect.area, у таблицы-объекта нет ключа area, поэтому она идёт искать его через __index у таблицы-класса, находит, и подставляет туда саму себя первым аргументом.
Малое отступление от метатаблиц: пример второго вызова — первое появление в данной статье двоеточия. Двоеточие — синтаксический сахар языка Lua. Если вызвать функцию в таблице через двоеточие, а не точку, первым аргументом в эту функцию подставится сама таблица, поэтому тот код эквивалентен.
Более подробно:
foo = {x = 10, y = 20}
function foo.bar(self, a, b)
return (self.x + a) * (self.y + b)
end
print('foo.bar(foo, 1, 2)', foo.bar(foo, 1, 2)) --> 242
-- Идентично, только self "автоматически" подставляется.
function foo:bar(a, b)
return (self.x + a) * (self.y + b)
end
print('foo:bar(1, 2)', foo:bar(1, 2)) --> 242
Можно попробовать слегка улучшить данный вариант.
Во-первых, добавить возможность вызывать класс как функцию с возвратом объекта,
во-вторых, добавить возможность перегрузки операторов у самого класса,
В третьих — наследование.
function Class(parent)
local class = {}
local mClass = {}
-- Сам класс будет метатаблицей для объектов.
-- Это позволит дописывать ему метаметоды.
class.__index = class
-- Поля объектов будут искаться по цепочке __index,
-- и дотянутся, в том числе, до родительского класса.
mClass.__index = parent
-- Резервируем поле Super под родителя.
class.Super = parent
-- Функция, которая будет вызываться при вызове класса
function mClass:__call(...)
local instance = setmetatable({}, class)
-- Резервируем поле класса "init"
if type(class.init) == 'function' then
-- Возвращаем экземпляр и всё что он вернул функцией init
return instance, instance:init(...)
end
-- Но если её нет - тоже ничего.
return instance
end
return setmetatable(class, mClass)
end
-- Основной класс.
Shape = Class()
function Shape:init(x, y)
-- в качестве self мы сюда передаём инстанс объекта.
self.x = x or 0
self.y = y or 0
return '!!!'
end
function Shape:posArea()
return self.x * self.y
end
-- Так как таблица Shape является метатаблицей для рождаемых ей объектов,
-- мы можем дописывать ей метаметоды.
function Shape:__tostring()
return '[' .. self.x .. ', ' .. self.y ..']'
end
local foo, value = Shape(10, 20)
print('foo, value', foo, value) --> [10, 20] !!!
-- Наследник
Rectangle = Class(Shape)
function Rectangle:init(x, y, w, h)
-- В данный момент, self - это пустая таблица,
-- к которой прицеплена таблица Rectangle, как мета.
-- Вызов родительских методов через Super.
self.Super.init(self, x, y)
-- Вызов собственных методов при инициализации - тоже возможен.
self.w, self.h = self:getDefault(w, h)
end
function Rectangle:area()
return self.w * self.h
end
function Rectangle:getDefault(w, h)
return w or 10, h or 20
end
function Rectangle:__tostring()
return '['..self.x..', '..self.y..', '..self.w .. ', '..self.h..']'
end
rect = Rectangle(10, 20, 30, 40)
print('rect', rect) --> [10, 20, 30, 40]
print('rect:area()' , rect:area()) --> 30 * 40 = 1200
-- Вызов родительского метода
print('rect:posArea()', rect:posArea()) --> 10 * 20 = 200
Таким образом, в 14 строк полезного кода, можно имплементировать в Lua максимум из действительно необходимого ООП.
Конечно, тут есть что улучшать и чем обвешивать, и подобная работа проведена в библиотеках middleclass или hump.Class, но иногда полезно и такое.
Кстати, если не хочется заморачиваться с функциями-классами, а нужно просто написать один-два класса, можно пользоваться конструкцией отсюда.
Прокси-таблицы
Вот тут, наконец-то, пример полноценной прокси, с отслеживанием действий.
function proxy()
local real_table = {}
local logger = {}
local metatable = {}
-- Забираем значения из настоящей таблицы и логируем их
function metatable:__index(key)
local value = rawget(real_table, key)
table.insert(logger, "Get key "..tostring(key)..' is '.. value)
return value
end
-- Подставляем значения в реальную таблицу, с произвольными действиями
function metatable:__newindex(key, value)
table.insert(logger, "Set key "..tostring(key)..' as '..tostring(value))
rawset(real_table, key, value)
end
return setmetatable({}, metatable), logger
end
prox, log = proxy()
prox.a = 10
prox.a = 20
print('prox.a', prox.a) --> 10
print('log', '\n'..table.concat(log, '\n'))
--> Set key a as 10
--> Set key a as 20
--> Get key a, is 20
На выходе, таблица которая логирует её использование.
В данном случае, таблица-прокси всегда пуста, в ней нет никаких ключей, поэтому __newindex будет вызываться каждый раз.
Таблицы временных объектов
Время от времени, могут понадобиться временные объекты, которые существуют некоторое время, но при нехватке памяти — освобождают занимаемое пространство. Данный пример потребует наличия библиотеки Luasec (https-запросы), хотя с тем же успехом можно использовать Luasocket, только без https.
page = {}
page.https = require'ssl.https'
page.cache = setmetatable({}, {__mode = 'v'})
-- Метатаблица для отдельных страничек
page._meta = {}
function page._meta:__tostring()
return 'Page: ['..self.url.. ']: '.. self.status
end
setmetatable(page, page)
function page:__call(url)
return self.cache[url] or self:request(url)
end
function page:request(url)
local page = setmetatable({}, self._meta)
page.url = url
page.data, page.status, page.error, page.hate = self.https.request(url)
print(page.data, page.status, page.error, page.hate)
self.cache[url] = page
return page
end
-- Запрашиваем, например, Яндекс.
p = page('https://yandex.ru')
print('p', p) --> Page: [https://yandex.ru]: 200
print('p.status', p.status) --> 200
-- И он болтается в кеше,
-- при последующих аналогичных запросах - будет извлекаться оттуда.
print('page.cache[...]', page.cache['https://yandex.ru']) --> Page: [https://yandex.ru]: 200
-- Он остаётся после сборки мусора, потому что остаётся сильная ссылка "p".
collectgarbage()
print('page.cache[...]', page.cache['https://yandex.ru']) --> Page: [https://yandex.ru]: 200
p = nil
collectgarbage()
-- Но после удаления ссылки - её больше нет, страничка больше не нужна.
print('page.cache[...]', page.cache['https://yandex.ru']) --> Nil
Пока всё
Я считаю, что данного материала достаточно чтобы более-менее освоить метатаблицы, если есть интересные или забавные примеры — пишите в комментариях.
Для желающих задать кучу вопросов — оставляю ссылку на чатик в тележке.