Что такое закрытие?

155

Время от времени я вижу упомянутое «замыкание», и я пытался найти его, но Вики не дает объяснения, которое я понимаю. Может ли кто-нибудь помочь мне здесь?

gablin
источник
Если вы знаете Java / C #, надеюсь, эта ссылка поможет
Гульшан
1
Закрытия трудно понять. Вы должны попробовать щелкнуть все ссылки в первом предложении этой статьи в Википедии и сначала понять эти статьи.
Зак
3
В чем принципиальная разница между замыканием и классом? Хорошо, класс только с одним открытым методом.
Бизиклоп
5
@biziclop: Вы можете эмулировать замыкание с помощью класса (это то, что должны делать разработчики Java). Но они обычно немного менее многословны для создания, и вам не нужно вручную управлять тем, что вы ищите. (Жесткие шепоты задают аналогичный вопрос, но, вероятно, приходят к другому выводу - поддержка ОО на уровне языка не нужна, когда у вас есть замыкания).

Ответы:

141

(Отказ от ответственности: это базовое объяснение; что касается определения, я немного упрощаю)

Самый простой способ думать о замыкании - это функция, которая может храниться как переменная (называемая «функцией первого класса»), которая имеет специальную возможность доступа к другим переменным, локальным для области, в которой она была создана.

Пример (JavaScript):

var setKeyPress = function(callback) {
    document.onkeypress = callback;
};

var initialize = function() {
    var black = false;

    document.onclick = function() {
        black = !black;
        document.body.style.backgroundColor = black ? "#000000" : "transparent";
    }

    var displayValOfBlack = function() {
        alert(black);
    }

    setKeyPress(displayValOfBlack);
};

initialize();

Функции 1 назначены document.onclickи displayValOfBlackявляются замыканиями. Вы можете видеть, что они оба ссылаются на логическую переменную black, но эта переменная назначается вне функции. Поскольку blackэто местное к области , где была определена функция , указатель на этот переменный сохраняется.

Если вы поместите это в HTML-страницу:

  1. Нажмите, чтобы изменить на черный
  2. Нажмите [enter], чтобы увидеть «true»
  3. Нажмите еще раз, снова становится белым
  4. Нажмите [enter], чтобы увидеть «false»

Это демонстрирует, что оба имеют доступ к одному black и тому же и могут использоваться для хранения состояния без какого-либо объекта-оболочки.

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

Замыкания обычно используются в качестве обработчиков событий, особенно в JavaScript и ActionScript. Хорошее использование замыканий поможет вам неявно связывать переменные с обработчиками событий, не создавая объектную оболочку. Однако неосторожное использование приведет к утечкам памяти (например, когда неиспользуемый, но сохраненный обработчик событий - единственное, что удерживает большие объекты в памяти, особенно объекты DOM, предотвращая сборку мусора).


1: На самом деле все функции в JavaScript являются замыканиями.

Николь
источник
3
Когда я читал твой ответ, я почувствовал, как в моей голове загорелась лампочка. Очень признателен! :)
Джей
1
Так blackкак объявлено внутри функции, разве это не будет уничтожено, когда стек раскручивается ...?
Габлин
1
@gablin, это то, что уникально в языках с замыканиями. Все языки со сборкой мусора работают практически одинаково - когда ссылки на объект больше не хранятся, он может быть уничтожен. Всякий раз, когда функция создается в JS, локальная область действия привязывается к этой функции, пока эта функция не будет уничтожена.
Николь
2
@ Габлин, это хороший вопрос. Я не думаю, что они не могут & mdash; но я только начал сборку мусора с тех пор, как использует то, что использует JS, и это то, на что вы, похоже, ссылались, когда говорили «Так blackкак объявлено внутри функции, разве это не будет уничтожено». Помните также, что если вы объявляете объект в функции, а затем присваиваете его переменной, которая находится где-то еще, этот объект сохраняется, потому что на него есть другие ссылки.
Николь
1
Objective-C (и C под Clang) поддерживает блоки, которые по сути являются замыканиями, без сборки мусора. Это требует поддержки во время выполнения и некоторого ручного вмешательства в управление памятью.
Quixoto
68

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

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

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

Мейсон Уилер
источник
3
+1: хороший ответ. Вы можете видеть замыкание как объект только с одним методом, а произвольный объект - как совокупность замыканий некоторых общих базовых данных (переменных-членов объекта). Я думаю, что эти два взгляда довольно симметричны.
Джорджио
3
Очень хороший ответ Это на самом деле объясняет понимание закрытия.
RoboAlex
1
@ Мейсон Уилер: Где хранятся данные закрытия? В стеке как функция? Или в куче как объект?
RoboAlex
1
@RoboAlex: В куче, потому что это объект, который выглядит как функция.
Мейсон Уилер
1
@RoboAlex: место закрытия и захваченных данных зависит от реализации. В C ++ он может храниться в куче или в стеке.
Джорджио
29

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

Возьмем для примера определение функции Scala:

def addConstant(v: Int): Int = v + k

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

Для того, чтобы оценить звонок как:

val n = addConstant(10)

мы должны присвоить kзначение, которое может произойти, только если имя kопределено в контексте, в котором addConstantоно определено. Например:

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  def addConstant(v: Int): Int = v + k

  values.map(addConstant)
}

Теперь, когда мы определились addConstantв контексте, где kон определен, addConstantон стал замыканием, потому что все его свободные переменные теперь закрыты (привязаны к значению): addConstantих можно вызывать и передавать, как если бы это была функция. Обратите внимание , что свободная переменная kпривязана к значению , когда крышка определена , тогда как переменная аргумент vсвязан , когда крышка вызывается .

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

На многих языках, если вы используете замыкание только один раз, вы можете сделать его анонимным , например,

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  values.map(v => v + k)
}

Обратите внимание, что функция без свободных переменных является частным случаем замыкания (с пустым набором свободных переменных). Аналогично, анонимная функция является частным случаем анонимного закрытия , то есть анонимная функция является анонимным закрытием без свободных переменных.

Джорджио
источник
Это хорошо согласуется с закрытыми и открытыми формулами в логике. Спасибо за Ваш ответ.
RainDoctor
@RainDoctor: Свободные переменные определяются в логических формулах и в выражениях лямбда-исчисления аналогичным образом: лямбда-выражение в лямбда-выражении работает как квантификатор в логических формулах по отношению к свободным / связанным переменным.
Джорджио
9

Простое объяснение в JavaScript:

var closure_example = function() {
    var closure = 0;
    // after first iteration the value will not be erased from the memory
    // because it is bound with the returned alertValue function.
    return {
        alertValue : function() {
            closure++;
            alert(closure);
        }
    };
};
closure_example();

alert(closure)будет использовать ранее созданное значение closure. Пространство alertValueимен возвращаемой функции будет связано с пространством имен, в котором находится closureпеременная. При удалении всей функции значение closureпеременной будет удалено, но до тех пор alertValueфункция всегда сможет прочитать / записать значение переменной closure.

Если вы запустите этот код, первая итерация присвоит closureпеременной значение 0 и перепишет функцию:

var closure_example = function(){
    alertValue : function(){
        closure++;
        alert(closure);
    }       
}

А поскольку для выполнения функции alertValueтребуется локальная переменная closure, она связывается со значением ранее назначенной локальной переменной closure.

И теперь каждый раз, когда вы вызываете closure_exampleфункцию, она будет записывать увеличенное значение closureпеременной, потому что alert(closure)она привязана.

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 
Муха
источник
спасибо, я не тестировал код =) теперь все в порядке.
Муха
5

«Закрытие» - это, по сути, некоторое локальное состояние и некоторый код, объединенные в пакет. Как правило, локальное состояние происходит из окружающей (лексической) области видимости, а код (по сути) является внутренней функцией, которая затем возвращается наружу. Закрытие - это комбинация захваченных переменных, которые видит внутренняя функция, и кода внутренней функции.

Это одна из тех вещей, которую, к сожалению, трудно объяснить из-за того, что она незнакома.

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

Vatine
источник
Вы забыли это: en.wikipedia.org/wiki/Closure_(computer_programming) в своем ответе.
С.Лотт
3
Нет, я сознательно решил не закрывать эту страницу.
Ватин
«Состояние и функция». Может ли функция C с staticлокальной переменной считаться замыканием? Завершения в Хаскеле связаны с государством?
Джорджио
2
@Giorgio Закрытия в Haskell действительно (я верю) близки к аргументам в лексической области, в которой они определены, поэтому я бы сказал «да» (хотя в лучшем случае я не знаком с Haskell). Функция AC со статической переменной является, в лучшем случае, очень ограниченным замыканием (вы действительно хотите иметь возможность создавать несколько замыканий из одной функции, с staticлокальной переменной, у вас точно одна).
Vatine
Я задал этот вопрос специально, потому что я думаю, что функция C со статической переменной не является замыканием: статическая переменная определяется локально и известна только внутри замыкания, она не имеет доступа к среде. Кроме того, я не уверен на 100%, но я бы сформулировал ваше утверждение наоборот: вы используете механизм замыкания для создания различных функций (функция является определением замыкания + привязка для ее свободных переменных).
Джорджио
5

Трудно определить, что такое замыкание, не определяя понятия «государство».

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

function foo(x)
return x
end

x = foo

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

Функционально он может решить многие из тех же проблем, что и ключевое слово «static» в C ++ (C?), Которое сохраняет состояние локальной переменной в течение нескольких вызовов функций; однако это больше похоже на применение этого же принципа (статической переменной) к функции, поскольку функции являются значениями первого класса; Закрытие добавляет поддержку для сохранения состояния всей функции (ничего общего со статическими функциями C ++).

Обработка функций как значений первого класса и добавление поддержки замыканий также означает, что в памяти может быть несколько экземпляров одной и той же функции (аналогично классам). Это означает, что вы можете повторно использовать один и тот же код без необходимости сбрасывать состояние функции, как это требуется при работе со статическими переменными C ++ внутри функции (может быть, это неправильно?).

Вот некоторое тестирование поддержки закрытия Lua.

--Closure testing
--By Trae Barlow
--

function myclosure()
    print(pvalue)--nil
    local pvalue = pvalue or 10
    return function()
        pvalue = pvalue + 10 --20, 31, 42, 53(53 never printed)
        print(pvalue)
        pvalue = pvalue + 1 --21, 32, 43(pvalue state saved through multiple calls)
        return pvalue
    end
end

x = myclosure() --x now references anonymous function inside myclosure()

x()--nil, 20
x() --21, 31
x() --32, 42
    --43, 53 -- if we iterated x() again

Результаты:

nil
20
31
42

Это может быть сложно, и это, вероятно, варьируется от языка к языку, но в Lua кажется, что всякий раз, когда функция выполняется, ее состояние сбрасывается. Я говорю это потому, что результаты из приведенного выше кода были бы другими, если бы мы обращались к myclosureфункции / состоянию напрямую (а не через анонимную функцию, которую она возвращает), как pvalueбыло бы возвращено к 10; но если мы получим доступ к состоянию myclosure через x (анонимная функция), вы увидите, что pvalueоно живо и хорошо где-то в памяти. Я подозреваю, что есть кое-что еще, возможно, кто-то может лучше объяснить природу реализации.

PS: я не знаю, что такое C ++ 11 (кроме того, что было в предыдущих версиях), поэтому учтите, что это не сравнение между замыканиями в C ++ 11 и Lua. Кроме того, все «линии, проведенные» из Lua в C ++, являются сходствами, поскольку статические переменные и замыкания не на 100% одинаковы; даже если они иногда используются для решения подобных проблем.

В чем я не уверен, так это в приведенном выше примере кода, является ли анонимная функция или функция более высокого порядка закрытой?

Trae Barlow
источник
4

Закрытие - это функция, связанная с состоянием:

В Perl вы создаете замыкания следующим образом:

#!/usr/bin/perl

# This function creates a closure.
sub getHelloPrint
{
    # Bind state for the function we are returning.
    my ($first) = @_;a

    # The function returned will have access to the variable $first
    return sub { my ($second) = @_; print  "$first $second\n"; };
}

my $hw = getHelloPrint("Hello");
my $gw = getHelloPrint("Goodby");

&$hw("World"); // Print Hello World
&$gw("World"); // PRint Goodby World

Если мы посмотрим на новую функциональность, предоставляемую C ++.
Также позволяет привязать текущее состояние к объекту:

#include <string>
#include <iostream>
#include <functional>


std::function<void(std::string const&)> getLambda(std::string const& first)
{
    // Here we bind `first` to the function
    // The second parameter will be passed when we call the function
    return [first](std::string const& second) -> void
    {   std::cout << first << " " << second << "\n";
    };
}

int main(int argc, char* argv[])
{
    auto hw = getLambda("Hello");
    auto gw = getLambda("GoodBye");

    hw("World");
    gw("World");
}
Мартин Йорк
источник
2

Давайте рассмотрим простую функцию:

function f1(x) {
    // ... something
}

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

В функциях верхнего уровня цепочка областей действия состоит из одного объекта - глобального объекта. Например, f1вышеприведенная функция имеет цепочку областей действия, в которой есть один объект, который определяет все глобальные переменные. (обратите внимание, что термин «объект» здесь не означает объект JavaScript, это просто объект, определенный реализацией, который действует как контейнер переменных, в котором JavaScript может «искать» переменные.)

Когда эта функция вызывается, JavaScript создает нечто, называемое «объект активации» , и помещает его в верхнюю часть цепочки областей действия. Этот объект содержит все локальные переменные (например, xздесь). Следовательно, теперь у нас есть два объекта в цепочке областей видимости: первый - это объект активации, а под ним - глобальный объект.

Обратите внимание, что два объекта помещаются в цепочку областей видимости в РАЗНОЕ время. Глобальный объект помещается, когда функция определена (т. Е. Когда JavaScript проанализировал функцию и создал объект функции), а объект активации входит, когда функция вызывается.

Итак, теперь мы знаем это:

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

Ситуация становится интересной, когда мы имеем дело с вложенными функциями. Итак, давайте создадим один:

function f1(x) {

    function f2(y) {
        // ... something
    }

}

Когда f1определяется, мы получаем цепочку областей действия, содержащую только глобальный объект.

Теперь, когда f1вызывается, цепочка областей действия f1получает объект активации. Этот объект активации содержит переменную xи переменную, f2которая является функцией. И обратите внимание, что f2это уже определено. Следовательно, на этом этапе JavaScript также сохраняет новую цепочку областей видимости f2. Цепочка области действия, сохраненная для этой внутренней функции, является текущей действующей цепью области действия. Действующая цепочка областей действия - это f1s. Следовательно f2, цепочка областей видимости - f1это текущая цепочка областей действия, которая содержит объект активации f1и глобальный объект.

Когда f2вызывается, он получает свой собственный объект активации, содержащий y, добавленный в его цепочку областей действия, которая уже содержит объект активации f1и глобальный объект.

Если бы внутри была определена другая вложенная функция f2, ее цепочка областей действия содержала бы три объекта во время определения (2 объекта активации двух внешних функций и глобальный объект) и 4 во время вызова.

Итак, теперь мы понимаем, как работает цепочка областей действия, но мы еще не говорили о замыканиях.

Комбинация объекта функции и области видимости (набора привязок переменных), в которой разрешаются переменные функции, в литературе по информатике называется замыканием - JavaScript - полное руководство Дэвида Фланагана

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

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

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

В нашем приведенном выше примере мы не возвращаемся f2из f1, следовательно, когда f1возвращается вызов , его объект активации будет удален из его цепочки областей видимости и собран мусор. Но если бы у нас было что-то вроде этого:

function f1(x) {

    function f2(y) {
        // ... something
    }

    return f2;
}

Здесь возвращаемый f2объект будет иметь цепочку области видимости, которая будет содержать объект активации f1, и, следовательно, он не будет собирать мусор. На данный момент, если мы вызовем f2, он сможет получить доступ f1к переменной, xдаже если мы вне f1.

Следовательно, мы можем видеть, что функция поддерживает цепочку областей видимости, а цепочка областей действия включает все объекты активации внешних функций. Это суть закрытия. Мы говорим, что функции в JavaScript «лексически ограничены» , что означает, что они сохраняют область, которая была активной, когда они были определены, в отличие от области, которая была активной, когда их вызывали.

Существует ряд мощных методов программирования, которые включают замыкания, такие как аппроксимация частных переменных, программирование на основе событий, частичное применение и т. Д.

Также обратите внимание, что все это относится ко всем тем языкам, которые поддерживают замыкания. Например, PHP (5.3+), Python, Ruby и т. Д.

treecoder
источник
-1

Закрытие - это оптимизация компилятора (он же синтаксический сахар?). Некоторые люди называют это Объектом Бедного Человека .

Смотрите ответ Эрика Липперта : (отрывок ниже)

Компилятор сгенерирует код следующим образом:

private class Locals
{
  public int count;
  public void Anonymous()
  {
    this.count++;
  }
}

public Action Counter()
{
  Locals locals = new Locals();
  locals.count = 0;
  Action counter = new Action(locals.Anonymous);
  return counter;
}

Есть смысл?
Кроме того, вы попросили сравнения. VB и JScript создают замыкания практически одинаково.

LamonteCristo
источник
Этот ответ - CW, потому что я не заслуживаю очков за отличный ответ Эрика. Пожалуйста, проголосуйте, как считаете нужным. HTH
goodguys_activate
3
-1: Ваше объяснение слишком корень в C #. Замыкание используется во многих языках и представляет собой нечто большее, чем синтаксический сахар в этих языках, и охватывает как функцию, так и состояние.
Мартин Йорк,
1
Нет, замыкание не является ни просто «оптимизацией компилятора», ни синтаксическим сахаром. -1