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

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

Примеры Index

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

На основе этого метаметода строится большое количество фич, таких как ООП, прокси-таблицы, дефолтные значения таблиц, и ещё много чего.

Иногда может быть вреден, когда необходимо получить точный ключ ВОТ ЭТОЙ таблицы, в таких случаях используют функцию value = rawget(table, key), которая является функцией доступа у таблиц по умолчанию (она, в байткоде, вызывается при попытке получения значения по ключу).

Что тут происходит:

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. Это — простейший пример добавления ключей через прокс-таблицу с помощью метаметода __newindex. Все новые ключи-значения, которые мы добавляем в таблицу bar, добавляются в foo в соответствии с функцией. Self, в данном случае — таблица bar;

2. __newindex распространяется только на несуществующие ключи;

3. Пример функции-фильтра, которая позволяет добавлять в таблицу только числовые значения. Точно так же можно проверять «добавляем только числовые ключи», или заранее создаём несколько таблиц для чисел-строк-таблиц и т.п, и добавляем значения в соответствующие (классификация/балансировка и т.п.).

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

1. Пример использования метатаблицы, таблицу можно вызывать как функцию. В качестве self передаётся сама таблица, вызванная как функция.

2. В данном примере, мы заполняем таблицу функциями, а метатаблица указывает, что если её вызовут как функцию — выдать результат фунции под ключом default.

Tostring и Concat

Просто приведение объекта к строке и конкатенация.

Metatable

Скрытие метатаблиц, иногда бывает полезно.

Строка, указывает режим связей значений таблиц. Если она содержит букву 'k', то слабыми будут объявлены ключи, если содержит букву 'v' — слабыми станут значения. Можно использовать их вместе. В примерах будет использоваться функция collectgarbage — принудительный сбор всего мусора.

Таблицы в Lua — всегда передаются по ссылке.

1. Пример таблицы слабых значений: если нет ссылок на значения, кроме как в этой таблице — они удалятся в процессе сборки мусора.

2. После исполнения данного участка кода, на таблицу "" существуют две ссылки (в глобальном пространстве и в таблице foo), а на таблицу "" — одна, внутри foo.

3. Мы видим, что пока в таблице foo есть все значения, но после сборки мусора — исчезает таблица key2. Это происходит потому, что на неё не осталось больше сильных ссылок, только слабые, которые позволяют мусорщику её собрать. К foo[1] это не относится, так как 100500 — не ссылочный тип (не таблица, не функция, не userdata и т.п, а число).

4. Если мы удалим единственную сильную ссылку bar — таблица тоже будет уничтожена после сборки мусора.

Аналогичным образом работает с 'k', только по отношению к ссылочным ключам таблицы (foo[] = true).

Функция __gc вызовется в том случае, если таблица будет собрана сборщиком мусора. Может использоваться как финалайзер. Функционирует с таблицами и cdata/userdata.

Функция, переопределяющая алгоритм вычисления длины таблицы (Lua 5.2+).

Pairs и Ipairs

Переопределение стандартных итераторов таблиц, для данной таблицы (Lua 5.2+).

Перегрузка операторов

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

1. Пример перегрузки операторов на таблицах, которые ведут себя как векторы, благодаря метатаблице. Следует следить за порядком аргументов, каждая операция — возвращает новую таблицу-«вектор».

2. Таблица, в которую можно добавлять элементы оператором "+". Порядок добавления определяет, в конец или в начало мы добавляем элемент.

3. 3 + 4 выполнится первым, поэтому первый элемент — «7». В остальных случаях — к результату выполнения предыдущего элемента будет прибавляться следующий: ((7 + foo -> foo) + 10 -> foo)…

Что со всем этим можно сделать

Первое что напрашивается — попытка сделать ООП.

Попробуем написать простую функцию, реализующую некоторый абстрактный «класс»:

Вот, уже что-то похожее на ООП. Тут нет наследования и всяких крутых штук, но это уже неплохо. При вызове rect.area, у таблицы-объекта нет ключа area, поэтому она идёт искать его через __index у таблицы-класса, находит, и подставляет туда саму себя первым аргументом.

Малое отступление от метатаблиц: пример второго вызова — первое появление в данной статье двоеточия. Двоеточие — синтаксический сахар языка Lua. Если вызвать функцию в таблице через двоеточие а не точку, первым аргументом в эту функцию подставится сама таблица, поэтому тот код эквивалентен.

Можно попробовать слегка улучшить данный вариант.

Во-первых, добавить возможность вызывать класс как функцию с возвратом объекта, во-вторых, добавить возможность перегрузки операторов у самого класса, В третьих — наследование.

Таким образом, в 15 строк полезного кода, можно имплементировать в Lua максимум из действительно необходимого ООП.

Конечно, тут есть что улучшать и чем обвешивать, и подобная работа проведена в библиотеках middleclass или hump.Class, но иногда полезно и такое.

Кстати, если не хочется заморачиваться с функциями-классами, а нужно просто написать один-два класса, можно пользоваться конструкцией отсюда.

Прокси-таблицы

Вот тут, наконец-то, пример полноценной прокси, с отслеживанием действий.

На выходе таблица, которая логирует её использование. В данном случае, таблица-прокси всегда пуста, в ней нет никаких ключей, поэтому __newindex будет вызываться каждый раз.

Таблицы временных объектов

Время от времени, могут понадобиться временные объекты, которые существуют некоторое время, но при нехватке памяти — освобождают занимаемое пространство. Данный пример потребует наличия библиотеки Luasec (https-запросы), хотя с тем же успехом можно использовать Luasocket, только без https.

Пока всё

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

📎📎📎📎📎📎📎📎📎📎