Недавно я посетил онлайн-курс по языкам программирования, на котором, помимо прочего, были представлены замыкания. Я записываю два примера, вдохновленных этим курсом, чтобы дать некоторый контекст, прежде чем задавать мой вопрос.
Первый пример - это функция SML, которая создает список чисел от 1 до x, где x - это параметр функции:
fun countup_from1 (x: int) =
let
fun count (from: int) =
if from = x
then from :: []
else from :: count (from + 1)
in
count 1
end
В SML REPL:
val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
countup_from1
Функция использует замыкание помощника , count
который собирает и использует переменную x
из контекста.
Во втором примере, когда я вызываю функцию create_multiplier t
, я возвращаю функцию (фактически, замыкание), которая умножает свой аргумент на t:
fun create_multiplier t = fn x => x * t
В SML REPL:
- fun create_multiplier t = fn x => x * t;
val create_multiplier = fn : int -> int -> int
- val m = create_multiplier 10;
val m = fn : int -> int
- m 4;
val it = 40 : int
- m 2;
val it = 20 : int
Таким образом, переменная m
связана с замыканием, возвращаемым вызовом функции, и теперь я могу использовать его по своему усмотрению.
Теперь, чтобы замыкание работало должным образом в течение всего его жизненного цикла, нам нужно продлить время жизни захваченной переменной t
(в данном примере это целое число, но оно может быть значением любого типа). Насколько я знаю, в SML это стало возможным благодаря сборке мусора: замыкание сохраняет ссылку на захваченное значение, которое впоследствии удаляется сборщиком мусора при уничтожении замыкания.
Мой вопрос: в общем, является ли сборка мусора единственным возможным механизмом, обеспечивающим безопасность затворов (вызываемых в течение всей их жизни)?
Или каковы другие механизмы, которые могли бы обеспечить достоверность замыканий без сбора мусора: скопировать захваченные значения и сохранить их внутри замыкания? Ограничить время жизни самого замыкания, чтобы оно не могло быть вызвано после истечения его перехваченных переменных?
Каковы наиболее популярные подходы?
РЕДАКТИРОВАТЬ
Я не думаю, что приведенный выше пример можно объяснить / реализовать, скопировав захваченную переменную (и) в замыкание. В общем, захваченные переменные могут быть любого типа, например, они могут быть связаны с очень большим (неизменным) списком. Таким образом, при реализации было бы очень неэффективно копировать эти значения.
Для полноты картины приведу еще один пример использования ссылок (и побочных эффектов):
(* Returns a closure containing a counter that is initialized
to 0 and is incremented by 1 each time the closure is invoked. *)
fun create_counter () =
let
(* Create a reference to an integer: allocate the integer
and let the variable c point to it. *)
val c = ref 0
in
fn () => (c := !c + 1; !c)
end
(* Create a closure that contains c and increments the value
referenced by it it each time it is called. *)
val m = create_counter ();
В SML REPL:
val create_counter = fn : unit -> unit -> int
val m = fn : unit -> int
- m ();
val it = 1 : int
- m ();
val it = 2 : int
- m ();
val it = 3 : int
Таким образом, переменные также могут быть захвачены по ссылке и остаются живыми после завершения вызова функции, создавшей их ( create_counter ()
).
источник
Ответы:
Язык программирования Rust интересен в этом аспекте.
Rust является системным языком с необязательным GC и изначально был разработан с замыканиями .
Как и другие переменные, ржавчины закрываются различными вкусами. Закрытия стека , наиболее распространенные, предназначены для одноразового использования. Они живут в стеке и могут ссылаться на что угодно. Собственные замыкания становятся владельцами захваченных переменных. Я думаю, что они живут на так называемой «куче обмена», которая является глобальной кучей. Их продолжительность жизни зависит от того, кому они принадлежат. Управляемые замыкания находятся в локальной куче задачи и отслеживаются GC задачи. Я не уверен насчет их ограничений по захвату.
источник
К сожалению, начиная с GC, вы становитесь жертвой синдрома XY:
Отметьте, однако, что идея продления времени жизни переменной не является необходимой для замыкания; это просто перенесено GC; исходное утверждение безопасности - только закрытые переменные должны жить столько же времени, сколько и замыкание (и даже если это шатко, мы можем сказать, что они должны существовать до последнего вызова замыкания).
По сути, есть два подхода, которые я вижу (и они могут быть объединены):
Последнее является просто симметричным подходом. Он используется не часто, но если, как и в Rust, у вас есть система типов, учитывающая регионы, то это, безусловно, возможно.
источник
Сборка мусора не требуется для безопасного закрытия при захвате переменных по значению. Одним из ярких примеров является C ++. C ++ не имеет стандартной сборки мусора. Лямбды в C ++ 11 являются замыканиями (они захватывают локальные переменные из окружающей области видимости). Каждая переменная, захваченная лямбда-выражением, может быть указана для захвата по значению или по ссылке. Если он захвачен ссылкой, то вы можете сказать, что это небезопасно. Однако, если переменная захвачена значением, это безопасно, потому что захваченная копия и исходная переменная являются отдельными и имеют независимые времена жизни.
В приведенном вами примере SML это легко объяснить: переменные фиксируются по значению. Нет необходимости «продлевать время жизни» любой переменной, потому что вы можете просто скопировать ее значение в замыкание. Это возможно, потому что в ML переменные не могут быть назначены. Таким образом, нет разницы между одной копией и многими независимыми копиями. Хотя в SML есть сборщик мусора, он не связан с захватом переменных с помощью замыканий.
Сборка мусора также не требуется для безопасного закрытия при захвате переменных по ссылке (вид). Одним из примеров является расширение Apple Blocks для языков C, C ++, Objective-C и Objective-C ++. В C и C ++ нет стандартной сборки мусора. Блоки захватывают переменные по значению по умолчанию. Однако, если локальная переменная объявлена с помощью
__block
, тогда блоки захватывают их, по-видимому, «по ссылке», и они безопасны - их можно использовать даже после области, в которой был определен блок. В данном случае__block
переменные на самом деле являются специальная структура внизу, и когда блоки копируются (блоки должны быть скопированы, чтобы использовать их вне области видимости в первую очередь), они «перемещают» структуру для__block
переменная в кучу, а блок управляет своей памятью, я считаю путем подсчета ссылок.источник
ref
). Итак, хорошо, можно спорить, связана ли реализация замыканий со сборкой мусора, но вышеприведенные утверждения следует исправить.ref
s, массивах и т. Д.), Которые указывают на структуру. Но ценность - это сама ссылка, а не то, на что она указывает. Если у вас есть,var a = ref 1
вы делаете копиюvar b = a
и используетеb
, значит ли это, что вы все еще используетеa
? Нет. У вас есть доступ к той же структуре, на которую указываетa
? Да. Именно так эти типы работают в SML и не имеют ничего общего с замыканиямиСборка мусора не требуется для реализации замыканий. В 2008 году язык Delphi, который не является сборщиком мусора, добавил реализацию замыканий. Это работает так:
Компилятор создает объект функтора под капотом, который реализует интерфейс, представляющий замыкание. Все закрытые локальные переменные заменяются с локальных для процедуры включения на поля объекта функтора. Это гарантирует, что состояние сохраняется до тех пор, пока есть функтор.
Ограничение этой системы состоит в том, что любой параметр, передаваемый по ссылке на вмещающую функцию, а также значение результата функции, не может быть захвачен функтором, поскольку они не являются локальными, область действия которых ограничена областью действия вмещающей функции.
На функтор ссылается ссылка замыкания, использующая синтаксический сахар, чтобы заставить разработчика выглядеть как указатель на функцию вместо интерфейса. Он использует систему подсчета ссылок Delphi для интерфейсов, чтобы гарантировать, что объект функтора (и все его состояние) остается «живым» столько, сколько ему нужно, а затем он освобождается, когда refcount падает до 0.
источник
shared_ptr
недетерминирована, потому что деструкторы стремятся к нулю.