2. Базовые концепции языка LUA

Этот раздел описывает основные концепции языка.

2.1 – Значения и типы

Lua — это динамически типизированный язык. Это означает, что переменные не имеют типов; только значения имеют типы. В языке отсутствуют определения типов. Все значения имеют свой собственный тип.

Все значения в Lua являются значениями первого класса. Это означает, что все значения могут храниться в переменных, передаваться в качестве аргументов другим функциям и возвращаться в качестве результатов.

В Lua есть восемь основных типов: nil, boolean, number, string, function, userdata, thread и table.

  • Тип nil имеет одно единственное значение — nil, основное свойство которого заключается в том, что оно отличается от любого другого значения; оно часто представляет отсутствие полезного значения.
  • Тип boolean имеет два значения: false и true. Оба nil и false делают условие ложным; их вместе называют ложными значениями. Любое другое значение делает условие истинным. Несмотря на свое название, false часто используется как альтернатива nil, с ключевым отличием в том, что false ведет себя как обычное значение в таблице, в то время как nil в таблице представляет отсутствующий ключ.
  • Тип number представляет как целые числа, так и вещественные (числа с плавающей запятой), используя два подтипа: integer и float. Стандартный Lua использует 64-битные целые числа и числа с двойной точностью (64 бита), но вы также можете скомпилировать Lua так, чтобы он использовал 32-битные целые числа и/или числа с одинарной точностью (32 бита). Опция с 32 битами как для целых чисел, так и для чисел с плавающей запятой особенно привлекательна для малых машин и встроенных систем. (Смотрите макрос LUA_32BITS в файле luaconf.h.)

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

Lua имеет явные правила о том, когда используется каждый подтип, но также автоматически выполняет преобразования между ними по мере необходимости (см. §3.4.3). Поэтому программист может в основном игнорировать разницу между целыми числами и числами с плавающей запятой или полностью контролировать представление каждого числа.

  • Тип string представляет собой неизменяемые последовательности байтов. Lua является 8-битным чистым языком: строки могут содержать любое 8-битное значение, включая встроенные нули (’\0’). Lua также не зависит от кодировки; он не делает предположений о содержимом строки. Длина любой строки в Lua должна помещаться в целое число Lua.

Lua может вызывать (и манипулировать) функциями, написанными как на Lua, так и на C (см. §3.4.10). Обе они представлены типом function.

  • Тип userdata предоставляется для хранения произвольных данных C в переменных Lua. Значение userdata представляет собой блок необработанной памяти. Существует два вида userdata: полное userdata, которое является объектом с блоком памяти, управляемым Lua, и легкое userdata, которое просто представляет собой значение указателя C. У userdata нет предопределенных операций в Lua, кроме присваивания и теста идентичности. С помощью метатаблиц программист может определить операции для значений полного userdata (см. §2.4). Значения userdata не могут быть созданы или изменены в Lua, только через C API. Это гарантирует целостность данных, принадлежащих хост-программе и библиотекам C.

  • Тип thread представляет собой независимые потоки выполнения и используется для реализации корутин (см. §2.6). Потоки Lua не связаны с потоками операционной системы. Lua поддерживает корутины на всех системах, даже на тех, которые не поддерживают потоки нативно.

  • Тип table реализует ассоциативные массивы, то есть массивы, которые могут иметь в качестве индексов не только числа, но и любое значение Lua, кроме nil и NaN. (Not a Number — это специальное значение с плавающей запятой, используемое стандартом IEEE 754 для представления неопределенных числовых результатов, таких как 0/0.) Таблицы могут быть гетерогенными; то есть они могут содержать значения всех типов (кроме nil). Любой ключ, связанный со значением nil, не считается частью таблицы. Напротив, любой ключ, который не является частью таблицы, имеет связанное значение nil.

Таблицы являются единственным механизмом структурирования данных в Lua; их можно использовать для представления обычных массивов, списков, символических таблиц, множеств, записей, графов, деревьев и т. д. Для представления записей Lua использует имя поля в качестве индекса. Язык поддерживает это представление, предоставляя a.name как синтаксический сахар для a[“name”]. В Lua есть несколько удобных способов создания таблиц (см. §3.4.9).

Как и индексы, значения полей таблицы могут быть любого типа. В частности, поскольку функции являются значениями первого класса, поля таблицы могут содержать функции. Таким образом, таблицы также могут содержать методы (см. §3.4.11).

Индексирование таблиц следует определению сырого равенства (raw equality) в языке. Выражения a[i] и a[j] обозначают один и тот же элемент таблицы тогда и только тогда, когда i и j сыро равны (то есть равны без учёта метаметодов).

В частности:

  • Числа с плавающей точкой, имеющие целочисленные значения, считаются равными соответствующим целым числам (например, 1.0 == 1).
  • Чтобы избежать неоднозначностей, любое число с плавающей точкой, используемое в качестве ключа и равное целому числу, преобразуется в это целое число.

Пример:
Если написать a[2.0] = true, то фактически в таблицу будет вставлен ключ 2 (целое число), а не 2.0.

Ключевые моменты:

  1. Сырое равенство (raw equality) — сравнение без метаметодов.
  2. Целые и дробные числа1.0 и 1 считаются одинаковыми ключами.
  3. Автоматическое преобразованиеa[2.0] превращается в a[2].

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

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

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

2.2 – Окружения и глобальное окружение

Имена переменных, области их видимости, правила лексического скрытия (shadowing) и работа с окружениями _ENV в языке Lua.

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

Все чанки (блоки кода) начинаются с неявного объявления global *, которое объявляет все свободные имена как глобальные переменные; это вводное объявление становится недействительным (void) внутри области видимости любого другого объявления global, как показано в следующем примере:

X = 1       -- Ok, глобальная по умолчанию
do
  global Y  -- отменяет неявное начальное объявление
  Y = 1     -- Ok, Y объявлена как глобальная
  X = 1     -- ОШИБКА, X не объявлена
end
X = 2       -- Ok, снова глобальная по умолчанию

Таким образом, вне любого объявления global Lua работает по принципу “глобальные по умолчанию”. Внутри любого объявления global Lua работает без умолчания: все переменные должны быть объявлены.

Lua — это язык с лексической областью видимости. Область видимости объявления переменной начинается с первого оператора после объявления и длится до последнего не-пустого (non-void) оператора самого внутреннего блока, включающего это объявление. (Пустыми операторами считаются метки и пустые инструкции.)

Объявление скрывает (shadowing) любое другое объявление с тем же именем, находящееся в контексте в точке объявления. Внутри этой тени любое внешнее объявление для этого имени становится недействительным. Смотрите следующий пример:

global print, x
x = 10                -- глобальная переменная
do                    -- новый блок
  local x = x         -- новая 'x' со значением 10
  print(x)            --> 10
  x = x+1
  do                  -- еще один блок
    local x = x+1     -- еще одна 'x'
    print(x)          --> 12
  end
  print(x)            --> 11
end
print(x)              --> 10  (глобальная)

Обратите внимание, что в объявлении вроде local x = x новая объявляемая x еще не находится в области видимости, поэтому x в правой части ссылается на внешнюю переменную.

Благодаря правилам лексической области видимости, локальные переменные могут свободно использоваться функциями, определенными внутри их области видимости. Локальная переменная, используемая внутренней функцией, называется upvalue (или внешней локальной переменной, или просто внешней переменной) внутри этой внутренней функции.

Обратите внимание, что каждое выполнение оператора local определяет новые локальные переменные. Рассмотрим следующий пример:

a = {}
local x = 20
for i = 1, 10 do
  local y = 0
  a[i] = function () y = y + 1; return x + y end
end

Цикл создает десять замыканий (то есть десять экземпляров анонимной функции). Каждое из этих замыканий использует свою отдельную переменную y, в то время как все они совместно используют одну и ту же x.

Как будет более подробно обсуждаться в §3.2 и §3.3.3, любая ссылка на глобальную переменную var синтаксически транслируется в _ENV.var. Более того, каждый чанк компилируется в области видимости внешней локальной переменной с именем _ENV (см. §3.3.2), поэтому само имя _ENV никогда не является свободным именем в чанке.

Несмотря на существование этой внешней переменной _ENV и трансляцию свободных имен, _ENV является обычным именем. В частности, вы можете определять новые переменные и параметры с этим именем. (Однако не следует определять _ENV как глобальную переменную, иначе _ENV.var транслировалось бы в _ENV._ENV.var и так далее, в бесконечном цикле.) Каждая ссылка на имя глобальной переменной использует ту _ENV, которая видна в данной точке программы.

Любая таблица, используемая в качестве значения _ENV, называется окружением (environment).

Lua поддерживает выделенное окружение, называемое глобальным окружением (global environment). Это значение хранится по специальному индексу в реестре C (см. §4.3). В Lua глобальная переменная _G инициализируется этим же значением. (_G никогда не используется внутри самого Lua, поэтому изменение ее значения повлияет только на ваш собственный код.)

Когда Lua загружает чанк, значением по умолчанию для его переменной _ENV является глобальное окружение (см. функцию load). Следовательно, по умолчанию глобальные переменные в коде Lua ссылаются на записи в глобальном окружении и, таким образом, ведут себя как обычные глобальные переменные. Более того, все стандартные библиотеки загружены в глобальное окружение, и некоторые функции там работают именно с этим окружением. Вы можете использовать load (или loadfile) для загрузки чанка с другим окружением. (В языке C для этого необходимо загрузить чанк, а затем изменить значение его первого upvalue; см. lua_setupvalue.)

2.3 – Обработка ошибок

Несколько операций в Lua могут вызывать ошибку. Ошибка прерывает нормальный поток выполнения программы, который может продолжиться, поймав ошибку.

Lua-код может явно вызвать ошибку, вызвав функцию error. (Эта функция никогда не возвращает значение.)

Чтобы поймать ошибки в Lua, вы можете выполнить защищенный вызов, используя pcall (или xpcall). Функция pcall вызывает заданную функцию в защищенном режиме. Любая ошибка во время выполнения функции останавливает ее выполнение, и управление немедленно возвращается в pcall, который возвращает код состояния.

Поскольку Lua является встроенным языком расширения, код Lua начинает выполняться по вызову из C-кода в хост-программе. (Когда вы используете Lua в автономном режиме, приложение lua является хост-программой.) Обычно этот вызов защищен; поэтому, когда происходит незащищенная ошибка во время компиляции или выполнения блока Lua, управление возвращается в хост, который может предпринять соответствующие меры, такие как вывод сообщения об ошибке.

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

Когда вы используете xpcall (или lua_pcall в C), вы можете передать обработчик сообщений, который будет вызван в случае ошибок. Эта функция вызывается с оригинальным объектом ошибки и возвращает новый объект ошибки. Она вызывается до того, как ошибка развернет стек, чтобы собрать больше информации об ошибке, например, проверяя стек и создавая трассировку стека. Этот обработчик сообщений также защищен защищенным вызовом; поэтому ошибка внутри обработчика сообщений снова вызовет обработчик сообщений. Если этот цикл продолжается слишком долго, Lua прерывает его и возвращает соответствующее сообщение. Обработчик сообщений вызывается только для обычных ошибок времени выполнения. Он не вызывается для ошибок выделения памяти и для ошибок при выполнении финализаторов или других обработчиков сообщений.

Lua также предлагает систему предупреждений (см. warn). В отличие от ошибок, предупреждения никоим образом не мешают выполнению программы. Обычно они просто генерируют сообщение для пользователя, хотя это поведение можно адаптировать из C (см. lua_setwarnf).

2.4 – Метатаблицы и метаметоды

Каждое значение в Lua может иметь метатаблицу. Эта метатаблица — это обычная таблица Lua, которая определяет поведение оригинального значения при определенных событиях. Вы можете изменить несколько аспектов поведения значения, установив конкретные поля в его метатаблице. Например, когда нечисловое значение является операндом сложения, Lua проверяет наличие функции в поле __add метатаблицы значения. Если она находит такую функцию, Lua вызывает ее для выполнения сложения.

Ключом для каждого события в метатаблице является строка с именем события, предшествующая двумя подчеркиваниями; соответствующее значение называется метазначением. Для большинства событий метазначение должно быть функцией, которая затем называется метаметодом. В предыдущем примере ключом является строка “__add”, а метаметодом — функция, выполняющая сложение. Если не указано иное, метаметодом может быть любое вызываемое значение, которое является либо функцией, либо значением с метаметодом __call.

Вы можете запросить метатаблицу любого значения, используя функцию getmetatable. Lua запрашивает метаметоды в метатаблицах, используя сырой доступ (см. rawget).

Вы можете заменить метатаблицу таблиц, используя функцию setmetatable. Вы не можете изменить метатаблицу других типов из кода Lua, кроме как с помощью библиотеки debug (§6.10).

Таблицы и полное userdata имеют индивидуальные метатаблицы, хотя несколько таблиц и userdata могут делить свои метатаблицы. Значения всех других типов делят одну единственную метатаблицу на тип; то есть, существует одна единственная метатаблица для всех чисел, одна для всех строк и т. д. По умолчанию значение не имеет метатаблицы, но библиотека строк устанавливает метатаблицу для типа строк (см. §6.4).

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

  • __add: операция сложения (+). Если любой операнд для сложения не является числом, Lua попытается вызвать метаметод. Он начинает с проверки первого операнда (даже если это число); если этот операнд не определяет метаметод для __add, то Lua проверит второй операнд. Если Lua находит метаметод, он вызывает этот метаметод с двумя операндами в качестве аргументов, и результат вызова (приведенный к одному значению) является результатом операции. В противном случае, если метаметод не найден, Lua вызывает ошибку.

  • __sub: операция вычитания (-). Поведение аналогично операции сложения.

  • __mul: операция умножения (*). Поведение аналогично операции сложения.

  • __div: операция деления (/). Поведение аналогично операции сложения.

  • __mod: операция взятия остатка (%) . Поведение аналогично операции сложения.

  • __pow: операция возведения в степень (^). Поведение аналогично операции сложения.

  • __unm: операция отрицания (унарный -). Поведение аналогично операции сложения.

  • __idiv: операция целочисленного деления (//). Поведение аналогично операции сложения.

  • __band: побитовая операция И (&). Поведение аналогично операции сложения, за исключением того, что Lua попытается вызвать метаметод, если любой операнд не является ни целым числом, ни числом с плавающей запятой, приводимым к целому (см. §3.4.3).

  • __bor: побитовая операция ИЛИ (|). Поведение аналогично побитовой операции И.

  • __bxor: побитовая операция исключающего ИЛИ (двойной ~). Поведение аналогично побитовой операции И.

  • __bnot: побитовая операция НЕ (унарный ~). Поведение аналогично побитовой операции И.

  • __shl: побитовый сдвиг влево («). Поведение аналогично побитовой операции И.

  • __shr: побитовый сдвиг вправо (»). Поведение аналогично побитовой операции И.

  • __concat: операция конкатенации (..). Поведение аналогично операции сложения, за исключением того, что Lua попытается вызвать метаметод, если любой операнд не является ни строкой, ни числом (которое всегда приводимо к строке).

  • __len: операция длины (#). Если объект не является строкой, Lua попытается вызвать его метаметод. Если метаметод существует, Lua вызывает его с объектом в качестве аргумента, и результат вызова (всегда приведенный к одному значению) является результатом операции. Если метаметода нет, но объект является таблицей, то Lua использует операцию длины таблицы (см. §3.4.7). В противном случае Lua вызывает ошибку.

  • __eq: операция равенства (==). Поведение аналогично операции сложения, за исключением того, что Lua попытается вызвать метаметод только тогда, когда сравниваемые значения являются либо обеими таблицами, либо обоими полными userdata и они не равны по примитивному сравнению. Результат вызова всегда преобразуется в логическое значение.

  • __lt: операция “меньше чем” (<). Поведение аналогично операции сложения, за исключением того, что Lua попытается вызвать метаметод только тогда, когда сравниваемые значения не являются ни обоими числами, ни обеими строками. Более того, результат вызова всегда преобразуется в логическое значение.

  • __le: операция “меньше или равно” (<=). Поведение аналогично операции “меньше чем”.

  • __index: операция доступа по индексу table[key]. Это событие происходит, когда table не является таблицей или когда ключ отсутствует в таблице. Метазначение ищется в метатаблице таблицы. Метазначение для этого события может быть либо функцией, либо таблицей, либо любым значением с метазначением __index. Если это функция, она вызывается с таблицей и ключом в качестве аргументов, и результат вызова (приведенный к одному значению) является результатом операции. В противном случае окончательный результат — это результат индексации этого метазначения с ключом. Эта индексация является обычной, а не сырой, и, следовательно, может вызвать другой метаметод __index.

  • __newindex: операция присваивания по индексу table[key] = value. Как и событие индексации, это событие происходит, когда table не является таблицей или когда ключ отсутствует в таблице. Метазначение ищется в метатаблице таблицы. Как и в случае с индексацией, метазначение для этого события может быть либо функцией, либо таблицей, либо любым значением с метазначением __newindex. Если это функция, она вызывается с таблицей, ключом и значением в качестве аргументов. В противном случае Lua повторяет присваивание индексации для этого метазначения с тем же ключом и значением. Это присваивание является обычным, а не сырым, и, следовательно, может вызвать другой метаметод __newindex.

Каждый раз, когда вызывается метазначение __newindex, Lua не выполняет примитивное присваивание. Если это необходимо, сам метаметод может вызвать rawset для выполнения присваивания.

  • __call: операция вызова func(args). Это событие происходит, когда Lua пытается вызвать значение, не являющееся функцией (то есть func не является функцией). Метаметод ищется в func. Если он присутствует, метаметод вызывается с func в качестве первого аргумента, за которым следуют аргументы оригинального вызова (args). Все результаты вызова являются результатами операции. Это единственный метаметод, который позволяет возвращать несколько результатов.

2.5 – Сборка мусора

Lua осуществляет автоматическое управление памятью. Это означает, что вам не нужно беспокоиться о выделении памяти для новых объектов или ее освобождении, когда объекты больше не нужны. Lua управляет памятью автоматически, запуская сборщик мусора для сбора всех мертвых объектов. Вся память, используемая Lua, подлежит автоматическому управлению: строки, таблицы, пользовательские данные (userdata), функции, потоки, внутренние структуры и т.д.

Объект считается мертвым, как только сборщик может быть уверен, что к объекту больше не будет обращений при нормальном выполнении программы. («Нормальное выполнение» здесь исключает финализаторы, которые воскрешают мертвые объекты (см. §2.5.3), а также исключает некоторые операции с использованием библиотеки отладки.) Обратите внимание, что момент, когда сборщик может быть уверен в том, что объект мертв, может не совпадать с ожиданиями программиста. Единственными гарантиями являются то, что Lua не будет собирать объект, к которому все еще можно обратиться при нормальном выполнении программы, и что в конечном итоге он соберет объект, который недоступен из Lua. (Здесь «недоступен из Lua» означает, что ни переменная, ни другой живой объект не ссылаются на данный объект.) Поскольку Lua ничего не знает о коде C, он никогда не собирает объекты, доступные через реестр (см. §4.3), который включает глобальное окружение (см. §2.2) и главный поток.

Сборщик мусора (GC) в Lua может работать в двух режимах: инкрементальном и поколенческом (generational).

Режим GC по умолчанию с параметрами по умолчанию подходит для большинства случаев использования. Однако программы, которые тратят большую часть своего времени на выделение и освобождение памяти, могут выиграть от других настроек. Имейте в виду, что поведение GC непереносимо как между платформами, так и между разными версиями Lua; следовательно, оптимальные настройки также непереносимы.

Вы можете изменить режим и параметры GC, вызвав lua_gc в C или collectgarbage в Lua. Вы также можете использовать эти функции для прямого управления сборщиком, например, для его остановки или перезапуска.

2.5.1 – Инкрементальная сборка мусора

В инкрементальном режиме каждый цикл GC выполняет сборку с пометкой и очисткой (mark-and-sweep) небольшими шагами, чередующимися с выполнением программы. В этом режиме сборщик использует три числа для управления своими циклами сборки мусора: паузу сборщика мусора, множитель шага сборщика мусора и размер шага сборщика мусора.

Пауза сборщика мусора управляет тем, как долго сборщик ждет перед началом нового цикла. Сборщик начинает новый цикл, когда количество байтов достигает n% от общего количества после предыдущей сборки. Большие значения делают сборщик менее агрессивным. Значения, равные или меньшие 100, означают, что сборщик не будет ждать начала нового цикла. Значение 200 означает, что сборщик ждет, пока общее количество байтов удвоится, прежде чем начать новый цикл.

Размер шага сборщика мусора управляет размером каждого инкрементального шага, а именно тем, сколько байтов выделяет интерпретатор перед выполнением шага: Значение n означает, что интерпретатор выделит примерно n байтов между шагами.

Множитель шага сборщика мусора управляет тем, сколько работы выполняет каждый инкрементальный шаг. Значение n означает, что интерпретатор выполнит n% единиц работы на каждое выделенное слово. Единица работы примерно соответствует обходу одного слота или очистке одного объекта. Большие значения делают сборщик более агрессивным. Остерегайтесь слишком малых значений, так как они могут сделать сборщик слишком медленным, чтобы вообще когда-либо завершить цикл. В качестве особого случая, нулевое значение означает неограниченную работу, фактически создавая неинкрементальный сборщик, останавливающий весь мир (stop-the-world).

2.5.2 – Поколенческая сборка мусора

В поколенческом режиме сборщик выполняет частые малые сборки, которые обходят только недавно созданные объекты. Если после малой сборки количество байтов превышает лимит, сборщик переключается на большую сборку, которая обходит все объекты. Затем сборщик продолжает выполнять большие сборки до тех пор, пока не обнаружит, что программа генерирует достаточно мусора, чтобы оправдать возврат к малым сборкам.

Поколенческий режим использует три параметра: множитель малой сборки (minor multiplier), множитель перехода от малой к большой сборке (minor-major multiplier) и множитель перехода от большой к малой сборке (major-minor multiplier).

Множитель малой сборки управляет частотой малых сборок. Для множителя x новая малая сборка будет выполнена, когда количество байтов станет на x% больше, чем количество используемых байтов сразу после последней большой сборки. Например, при множителе 20 сборщик выполнит малую сборку, когда количество байтов станет на 20% больше, чем общее количество после последней большой сборки.

Множитель перехода от малой к большой сборке управляет переходом к большим сборкам. Для множителя x сборщик переключится на большую сборку, когда количество байтов от старых объектов станет на x% больше, чем общее количество после предыдущей большой сборки. Например, при множителе 100 сборщик выполнит большую сборку, когда количество старых байтов превысит удвоенное общее количество после предыдущей большой сборки. В качестве особого случая, значение 0 останавливает выполнение сборщиком больших сборок.

Множитель перехода от большой к малой сборке управляет возвратом к малым сборкам. Для множителя x сборщик вернется к малым сборкам после того, как большая сборка соберет не менее x% байтов, выделенных во время последнего цикла. В частности, при множителе 0 сборщик немедленно вернется к малым сборкам после выполнения одной большой сборки.

2.5.3 – Метаметоды сборки мусора

Вы можете установить метаметоды сборщика мусора для таблиц и, используя C API, для полных пользовательских данных (userdata) (см. §2.4). Эти метаметоды, называемые финализаторами, вызываются, когда сборщик мусора обнаруживает, что соответствующая таблица или userdata мертвы. Финализаторы позволяют вам координировать сборку мусора Lua с управлением внешними ресурсами, такими как закрытие файлов, сетевых соединений или соединений с базами данных, или освобождение вашей собственной памяти.

Чтобы объект (таблица или userdata) был финализирован при сборе, вы должны пометить его для финализации. Вы помечаете объект для финализации, когда устанавливаете его метатаблицу, и эта метатаблица имеет метаметод __gc. Обратите внимание, что если вы установите метатаблицу без поля __gc и позже создадите это поле в метатаблице, объект не будет помечен для финализации.

Когда помеченный объект становится мертвым, он не собирается сборщиком мусора немедленно. Вместо этого Lua помещает его в список. После сборки Lua проходит по этому списку. Для каждого объекта в списке он проверяет метаметод __gc объекта: Если он присутствует, Lua вызывает его, передавая объект в качестве единственного аргумента.

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

Поскольку собираемый объект все еще должен использоваться финализатором, этот объект (и другие объекты, доступные только через него) должны быть воскрешены Lua. Обычно это воскрешение является временным, и память объекта освобождается в следующем цикле сборки мусора. Однако, если финализатор сохраняет объект в каком-либо глобальном месте (например, в глобальной переменной), то воскрешение становится постоянным. Более того, если финализатор снова помечает финализируемый объект для финализации, его финализатор будет вызван снова в следующем цикле, когда объект будет мертв. В любом случае память объекта освобождается только в том цикле GC, когда объект мертв и не помечен для финализации.

Когда вы закрываете состояние (см. lua_close), Lua вызывает финализаторы всех объектов, помеченных для финализации, следуя обратному порядку их пометки. Если какой-либо финализатор помечает объекты для сбора на этом этапе, эти пометки не имеют эффекта.

Финализаторы не могут прерываться (yield) и не могут запускать сборщик мусора. Поскольку они могут выполняться в непредсказуемые моменты времени, хорошей практикой является ограничение каждого финализатора минимумом, необходимым для надлежащего освобождения связанного с ним ресурса.

Любая ошибка во время выполнения финализатора генерирует предупреждение; ошибка не распространяется дальше.

2.5.4 – Слабые таблицы

Слабая таблица — это таблица, элементы которой являются слабыми ссылками. Слабая ссылка игнорируется сборщиком мусора. Другими словами, если единственными ссылками на объект являются слабые ссылки, то сборщик мусора соберет этот объект.

Слабая таблица может иметь слабые ключи, слабые значения или и то, и другое. Таблица со слабыми значениями позволяет собирать свои значения, но предотвращает сбор своих ключей. Таблица со слабыми и ключами, и значениями позволяет собирать как ключи, так и значения. В любом случае, если либо ключ, либо значение собраны, вся пара удаляется из таблицы. Слабость таблицы контролируется полем __mode ее метатаблицы. Это метазначение, если присутствует, должно быть одной из следующих строк: "k" для таблицы со слабыми ключами; "v" для таблицы со слабыми значениями; или "kv" для таблицы со слабыми и ключами, и значениями.

Таблица со слабыми ключами и сильными значениями также называется эфемеронной таблицей (ephemeron table). В эфемеронной таблице значение считается достижимым, только если достижим его ключ. В частности, если единственная ссылка на ключ исходит из его значения, пара удаляется.

Любое изменение слабости таблицы может вступить в силу только на следующем цикле сбора. В частности, если вы меняете слабость на более сильный режим, Lua все еще может собрать некоторые элементы из этой таблицы до того, как изменение вступит в силу.

Только объекты, имеющие явное построение, удаляются из слабых таблиц. Значения, такие как числа и легкие C-функции, не подлежат сборке мусора и, следовательно, не удаляются из слабых таблиц (если только не собраны связанные с ними значения). Хотя строки подлежат сборке мусора, они не имеют явного построения, и их равенство определяется по значению; они ведут себя скорее как значения, чем как объекты. Поэтому они не удаляются из слабых таблиц.

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

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

2.6 – Сопрограммы

Lua поддерживает сопрограммы (coroutines), также называемые совместной многопоточностью. Сопрограмма в Lua представляет собой независимый поток выполнения. Однако, в отличие от потоков в многопоточных системах, сопрограмма приостанавливает свое выполнение только путем явного вызова функции прерывания (yield).

Вы создаете сопрограмму, вызывая coroutine.create. Ее единственным аргументом является функция, которая является главной функцией сопрограммы. Функция create только создает новую сопрограмму и возвращает дескриптор на нее (объект типа thread); она не запускает сопрограмму.

Вы выполняете сопрограмму, вызывая coroutine.resume. Когда вы впервые вызываете coroutine.resume, передавая в качестве первого аргумента поток, возвращенный coroutine.create, сопрограмма начинает свое выполнение с вызова своей главной функции. Дополнительные аргументы, переданные в coroutine.resume, передаются в качестве аргументов этой функции. После того как сопрограмма начинает работать, она выполняется до тех пор, пока не завершится или не прервется (yield).

Сопрограмма может завершить свое выполнение двумя способами: нормально, когда ее главная функция возвращает управление (явно или неявно, после последней инструкции); и аварийно, если происходит незащищенная ошибка. В случае нормального завершения coroutine.resume возвращает true плюс любые значения, возвращенные главной функцией сопрограммы. В случае ошибок coroutine.resume возвращает false плюс объект ошибки. В этом случае сопрограмма не разворачивает свой стек, так что после ошибки его можно проверить с помощью API отладки.

Сопрограмма прерывается (yield) вызовом coroutine.yield. Когда сопрограмма прерывается, соответствующий coroutine.resume возвращается немедленно, даже если прерывание происходит внутри вложенных вызовов функций (то есть не в главной функции, а в функции, прямо или косвенно вызванной главной функцией). В случае прерывания coroutine.resume также возвращает true плюс любые значения, переданные в coroutine.yield. В следующий раз, когда вы возобновите ту же сопрограмму, она продолжит свое выполнение с точки прерывания, при этом вызов coroutine.yield вернет любые дополнительные аргументы, переданные в coroutine.resume.

Как и coroutine.create, функция coroutine.wrap также создает сопрограмму, но вместо возврата самой сопрограммы она возвращает функцию, которая при вызове возобновляет сопрограмму. Любые аргументы, переданные этой функции, идут как дополнительные аргументы в coroutine.resume. coroutine.wrap возвращает все значения, возвращенные coroutine.resume, кроме первого (булева кода ошибки). В отличие от coroutine.resume, функция, созданная coroutine.wrap, распространяет любую ошибку вызывающей стороне. В этом случае функция также закрывает сопрограмму (см. coroutine.close).

В качестве примера того, как работают сопрограммы, рассмотрим следующий код:

function foo (a)
  print("foo", a)
  return coroutine.yield(2*a)
end

co = coroutine.create(function (a,b)
      print("co-body", a, b)
      local r = foo(a+1)
      print("co-body", r)
      local r, s = coroutine.yield(a+b, a-b)
      print("co-body", r, s)
      return b, "end"
end)

print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))

Когда вы запустите его, он выдаст следующий вывод:

co-body 1       10
foo     2
main    true    4
co-body r
main    true    11      -9
co-body x       y
main    true    10      end
main    false   cannot resume dead coroutine

Вы также можете создавать сопрограммы и управлять ими через C API: см. функции lua_newthread, lua_resume и lua_yield.