#lua #closures #implementation
#lua #закрытия #реализация #замыкания
Вопрос:
У меня есть вопрос о том, как реализованы замыкания.
Скажем, это находится в файле с именем test.lua
:
local a = 'asdf'
local function b()
return a
end
a = 10
return b
И другой файл делает
a = require 'test'
a()
он будет печатать
10
Если a
в стеке есть указатель на 'asdf'
(я полагаю, в куче, но это не имеет значения), и замыкание b
создано, так что, предположительно, адрес, который был в, a
сохраняется для b
использования, как a = 10
изменить указатель внутри замыкания?
В Википедии довольно хорошо сказано, что меня озадачивает:
Реализация языка не может легко поддерживать полное замыкание, если его модель памяти во время выполнения распределяет все локальные переменные в линейном стеке1. В таких языках локальные переменные функции освобождаются при возврате функции.
Я подумал, что, возможно, b
на самом деле сохранен не указатель на 'asdf'
, а смещение стека на a
, так что вы можете изменить a
, и смещение стека приведет вас к тому, a
что указывает на последнее, что вы установили a
, но тогда как это работает, когда a
(указатель) извлекается из стека и смещение стека становится недействительным?
1 Я знаю, что Lua не выделяет значения в стеке, но он выделяет локальные указатели в стеке на значения в куче, не так ли?
Комментарии:
1. Я изменил заголовок и теги, чтобы сделать этот вопрос специфичным для LUA. То, как реализуются замыкания (и поддерживаются ли вообще замыкания), очень зависит от языка.
2. @pst ну, lua реализован на C, поэтому я подумал, что это актуально.
3. Дело в том, что кодеры [только] на C не будут знать или заботиться — «это вещь LUA» 😉
Ответ №1:
Я действительно хотел бы, чтобы вы назвали эти переменные немного более разумно. Итак, я буду:
local inner = 'asdf'
local function b()
return inner
end
inner = 10
return b
и
func = require 'test'
func()
Хорошо, теперь, когда мы знаем, о чем говорим, я могу продолжить.
Блок Lua test
имеет локальную переменную с именем inner
. Внутри этого фрагмента вы создаете новую функцию b
. Поскольку это новая функция, ее область действия находится в пределах области действия блока test
.
Поскольку он находится внутри функции, он имеет право доступа к локальным переменным, объявленным вне этой функции. Но поскольку он находится внутри функции, он не обращается к этим переменным, как к одной из своих собственных локальных. Компилятор обнаруживает, что inner
это локальная переменная, объявленная вне области видимости функции, поэтому он преобразует ее в то, что Lua называет «повышающим значением».
Функции в Lua могут иметь произвольное количество значений (до 255), связанных с ними, называемых «повышающими значениями». Функции, созданные на C / C , могут хранить некоторое количество повышающих значений с помощью lua_pushcclosure
. Функции, созданные компилятором Lua, используют значения up для обеспечения лексической области видимости.
Область действия — это все, что происходит внутри фиксированного блока кода Lua. Итак:
if(...) then
--yes
else
--no
end
yes
Блок имеет область действия, а no
блок имеет другую область действия. Любые local
переменные, объявленные в yes
блоке, не могут быть доступны из no
блока, поскольку они находятся за пределами области действия no
блока.
Конструкциями Lua, которые определяют область видимости, являются if/then/else/end
, while/do/end
, repeat/until
do/end
, for/end
function/end
, и,,. Кроме того, каждый скрипт, называемый Lua «фрагментом», имеет область видимости.
Области являются вложенными. Из одной области вы можете получить доступ к локальным переменным, объявленным в более высокой области.
«Стек» представляет все переменные, объявленные как local
в определенной области. Таким образом, если у вас нет локальных переменных в определенной области, стек для этой области пуст.
В C и C «стек», с которым вы знакомы, — это просто указатель. Когда вы вызываете функцию, компилятор заранее определяет, сколько байт пространства требуется стеку функции. Он продвигает указатель на эту величину. Все переменные стека, используемые в функции, являются просто байтовыми смещениями от указателя стека. Когда функция завершает работу, указатель стека уменьшается на величину стека.
В Lua все по-другому. Стек для конкретной области является объектом, а не просто указателем. Для любой конкретной области видимости для нее определено некоторое количество local
переменных. Когда интерпретатор Lua входит в область видимости, он «выделяет» стек размера, необходимого для доступа к этим локальным переменным. Все ссылки на локальные переменные являются просто смещениями в этот стек. Доступ к локальным переменным из более высоких областей (ранее определенных) просто обращается к другому объекту стека.
Итак, в Lua у вас концептуально есть стек стеков (который я буду называть «s-stack» для ясности). Каждая область создает новый стек и выталкивает его, и когда вы покидаете область, он выталкивает стек из s-стека.
Когда компилятор Lua обнаруживает ссылку на local
переменную, он преобразует эту ссылку в индекс в s-стеке и смещение в этот конкретный стек. Итак, если он обращается к переменной в текущем локальном стеке, индекс в s-стеке ссылается на вершину s-стека, а смещение — это смещение в том стеке, где находится переменная.
Это нормально для большинства конструкций Lua, которые обращаются к областям. Но function/end
не просто создают новую область; они создают новую функцию. И этой функции разрешен доступ к стекам, которые не являются просто локальным стеком этой функции.
Стеки — это объекты. А в Lua объекты подлежат сборке мусора. Когда интерпретатор входит в область видимости, он выделяет объект стека и выталкивает его. Пока объект стека помещен в s-стек, он не может быть уничтожен. Стек стеков ссылается на объект. Однако, как только интерпретатор выходит из области видимости, он извлекает стек из s-стека. Итак, поскольку на него больше нет ссылок, он подлежит сбору.
Однако функция, которая обращается к переменным за пределами своей собственной локальной области видимости, все еще может ссылаться на этот стек. Когда компилятор Lua видит ссылку на local
переменную, которая не входит в локальную область действия функции, это изменяет функцию. Он определяет, к какому стеку принадлежит локальный, на который он ссылается, и затем сохраняет этот стек как значение up в функции. Он преобразует ссылку на эту переменную в смещение в это конкретное значение upvalue, а не смещение в стек, который в данный момент находится в s-стеке.
Таким образом, до тех пор, пока объект функции продолжает существовать, будут существовать и стеки, на которые он ссылается.
Помните, что стеки динамически создаются и уничтожаются по мере того, как интерпретатор Lua входит в область функций и выходит из нее. Таким образом, если бы вы запустили test
дважды, вызвав loadfile
и выполнив возвращаемую функцию дважды, вы бы получили две отдельные функции, которые ссылаются на два отдельных стека. Ни одна из функций не увидит значение от другой.
Обратите внимание, что это может быть не совсем так, как это реализовано, но такова общая идея, стоящая за этим.
Комментарии:
1. Ага, понятно. Итак, тогда, если каждая функция сохраняет свою собственную область видимости, а компилятор преобразует имена локальных переменных в смещения в этой области, в стеке никогда не будет локальных переменных, потому что они там просто не хранятся, правильно? (Не считая нажатия на них для вызовов функций и т.д.)
2. А также, многие области могут быть связаны с одной функцией, правильно? Это собственное, и области, к которым относятся значения, на которые оно ссылается.
3. @SethCarnegie:
local
переменные — это переменные, которые являются локальными для определенной области. Эта область является стеком; стек — это объект. При выходе из области видимости ссылка на этот стек теряется. Если ничто другое не владеет ссылкой на этот стек (если ни одна из существующих функций не ссылается на стек), то стек может быть собран. Замыкания хранят часть стека. Можно было бы ожидать, что умный компилятор может определить, когда функция сохранит часть стека, а когда нет, поэтому он может оптимизировать простые случаи.4. @SethCarnegie: Внутренний стек функции (тот, из которого она получает свои собственные
local
переменные) работает как обычно. Оно возникает при вызове функции и исчезает при завершении функции. Внешние стеки, на которые он ссылается, являются частью самого объекта функции, но стек выполнения функции перестраивается при каждом вызове функции.5. Я имел в виду глобальный стек. Я не знал, что у каждой функции есть свой собственный стек. Или, скорее, я не знал, что область действия называется стеком, поскольку кажется, что это будет фиксированный размер, поскольку вы точно знаете, сколько локальных переменных у вас будет во время «компиляции». Итак, существует глобальный стек для всей программы и мини-стек (область видимости) для каждой функции при ее вызове, правильно? Спасибо, что поддерживаете меня.