Lua. Краткое введение в метатаблицы для чайников

pwh-kakokqutguzo8rusfsaqezm.png

На написание данной статьи меня сподвигло большое количество вопросов по метатаблицам и ООП в Lua, благо это самый сложный и проблематичный раздел у изучающих данный язык, но, так как Lua проектировалась как язык для начинающих и не-программистов и, в целом, имеет небольшой объём материала для освоения, негоже оставлять её «на потом», учитывая что с помощью метатаблиц можно творить чудеса и крайне элегантные решения заковыристых задач.

В данной публикации будет описание всех стандартных мета-методов для версий Lua 5.1–5.3, с примерами. Начнём.

Метатаблицы


Что это такое?
На самом деле, метатаблица ничем не отличается от обычной таблицы, за исключением того что она указана как управляющая.

Схематично можно представить, например, так:
ndeo14kgyseidba6hp6xorhpkkw.png

В метатаблице описывается реакция основной таблицы на воздействия, например, вызов таблицы как функцию, деление таблицы на произвольное значение, или попытка вытянуть из неё ключ, которого у неё нет, благодаря специальным ключам (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»!
jsmqprpne1ts9mqlhcjxhxy0cj4.png

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

Пока всё

Я считаю, что данного материала достаточно чтобы более-менее освоить метатаблицы, если есть интересные или забавные примеры — пишите в комментариях.

Для желающих задать кучу вопросов — оставляю ссылку на чатик в тележке.

© Habrahabr.ru