Що таке закриття?


155

Час від часу я бачу, як згадуються "закриття", і я намагався шукати це, але Wiki не дає пояснення, яке я розумію. Може хтось допоможе мені тут?


Якщо ви знаєте, що Java / C # сподіваюся, що це посилання допоможе- http://www.developerfusion.com/article/8251/the-beauty-of-closures/
Гюльшан

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
Під час читання вашої відповіді я відчув, як у моїй свідомості вмикається лампочка. Цінується! :)
Jay

1
Оскільки blackце оголошено всередині функції, чи не буде це знищено, коли стек розкручується ...?
габлін

1
@gablin, саме це є унікальним для мов, які мають закриття. Всі мови зі збиранням сміття працюють майже однаково - коли більше ніяких посилань на об'єкт не проводиться, його можна знищити. Кожного разу, коли функція створена в JS, локальна область дії пов'язана з цією функцією, поки ця функція не буде знищена.
Ніколь

2
@gablin, це гарне запитання. Я не думаю, що вони не можуть & mdash; але я виховував лише збирання сміття з того, що використовує JS, і це те, на що ви, мабуть, посилаєтесь, коли ви сказали "Оскільки blackце оголошено всередині функції, не було б це знищено". Пам'ятайте також, що якщо ви оголошуєте об'єкт у функції, а потім присвоюєте йому змінну, яка живе десь в іншому місці, цей об'єкт зберігається, оскільки на нього є інші посилання.
Ніколь

1
Objective-C (і C під клатчем) підтримує блоки, які по суті є закриттями, без збору сміття. Це вимагає підтримки часу виконання та певного ручного втручання навколо управління пам'яттю.
Кіхото

68

Закриття - це лише інший спосіб погляду на предмет. Об'єкт - це дані, які мають одну чи більше функцій, пов'язаних із ним. Закриття - це функція, яка має одну або кілька змінних, прив'язаних до неї. Два в основному однакові, принаймні на рівні реалізації. Справжня різниця полягає в тому, звідки вони беруться.

У об'єктно-орієнтованому програмуванні ви оголошуєте об’єктний клас, визначаючи його змінні учасника та його методи (функції члена) вперед, а потім створюєте екземпляри цього класу. Кожен екземпляр постачається з копією даних члена, ініціалізованою конструктором. Потім у вас є змінна типу об'єкта і передаєте її навколо як фрагмент даних, оскільки в центрі уваги є її природа як дані.

З іншого боку, об'єкт не визначається наперед, як клас об’єкта, або інстанціюється через виклик конструктора у вашому коді. Натомість ви записуєте закриття як функцію всередині іншої функції. Закриття може посилатися на будь-яку з локальних змінних зовнішньої функції, і компілятор виявляє, що переміщує ці змінні з простору стека зовнішньої функції в декларування прихованого об'єкта закриття. Потім у вас є змінна типу закриття, і, хоча це в основному об'єкт під кришкою, ви передаєте його навколо як функції посилання, оскільки фокус робиться на його природі як функції.


3
+1: Гарна відповідь. Ви можете бачити закриття як об'єкт лише одним методом, а довільний об'єкт як набір закриттів над деякими загальними базовими даними (змінними-членами об'єкта). Я думаю, що ці два погляди є досить симетричними.
Джорджіо

3
Дуже гарна відповідь. Це фактично пояснює уявлення про закриття.
RoboAlex

1
@ Mason Wheeler: Де зберігаються дані про закриття? У стеці як функція? Або в купі, як предмет?
RoboAlex

1
@RoboAlex: У купі, тому що це об'єкт, схожий на функцію.
Мейсон Уілер

1
@RoboAlex: Де зберігається закриття та його захоплені дані, залежить від реалізації. У C ++ він може зберігатися в купі або в стеці.
Джорджіо

29

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

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

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

У тілі функції є два імені (змінних) vі kвказують два цілих значення. Назва vпов'язана, оскільки вона оголошена як аргумент функції addConstant(дивлячись на декларацію функції, ми знаємо, що vпри виклику функції буде присвоєно значення). Назва kє вільною функцією wrt, 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: Вільні змінні визначаються у логічних формулах та виразах лямбда-числення аналогічним чином: лямбда в виразі лямбда працює як квантор у логічних формулах wrt вільних / обмежених змінних.
Джорджіо

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. 

дякую, я не перевіряв код =) зараз все здається нормальним.
Muha

5

"Закриття" - це, по суті, місцевий стан і якийсь код, об'єднані в пакет. Зазвичай локальний стан походить із оточуючої (лексичної) сфери дії, і код є (по суті) внутрішньою функцією, яка потім повертається назовні. Закриття - це комбінація захоплених змінних, яку бачить внутрішня функція, та код внутрішньої функції.

Це одна з тих речей, яку, на жаль, трохи важко пояснити через незнайомість.

Одна з аналогій, яку я успішно використовував у минулому, була "уявіть, що у нас є щось, що ми називаємо" книгою ", в приміщенні-закритті," книга "- це копія там, у кутку, TAOCP, але на закритті столу. , це та копія книги "Дрезденські файли". Отже, залежно від того, в якому закритті ви знаходитесь, код "дай мені книгу" призводить до різних випадків ".


Ви забули це: en.wikipedia.org/wiki/Closure_(computer_programming) у своїй відповіді.
С.Лотт

3
Ні, я сумлінно вирішив не закривати цю сторінку.
Ватін

"Стан і функція". Чи staticможна вважати функцію C з локальною змінною закриттям? Чи передбачають закриття в Хаскелл державі?
Джорджіо

2
@ Giorgio Closures в Haskell робити (я вважаю) закриває аргументи в тій лексичній області, яку вони визначають, тому я б сказав "так" (хоча я в кращому випадку не знайомий з Haskell). Функція змінного струму зі статичною змінною в кращому випадку є дуже обмеженим закриттям (ви дійсно хочете мати можливість створити декілька закриттів із однієї функції, з staticлокальною змінною у вас є саме одна).
Ватін

Я задав це питання спеціально, тому що думаю, що функція C зі статичною змінною не є закриттям: статична змінна визначається локально і відома лише всередині замикання, вона не має доступу до навколишнього середовища. Крім того, я не впевнений на 100%, але я б сформулював ваше твердження навпаки: ви використовуєте механізм закриття для створення різних функцій (функція - це визначення закриття + прив'язка для його вільних змінних).
Джорджіо

5

Важко визначити, що таке закриття, не визначаючи поняття "держава".

В основному, з мовою з повним лексичним визначенням, яка розглядає функції як значення першого класу, відбувається щось особливе. Якби я робив щось на кшталт:

function foo(x)
return x
end

x = foo

Змінна xне тільки посилається, function foo()але також посилається на стан, яке fooбуло залишено в останній раз, коли воно поверталося. Справжня магія буває тоді, коли fooв її межах додатково визначені інші функції; це як власне міні-середовище (так само, як "нормально" ми визначаємо функції у глобальному середовищі).

Функціонально він може вирішити багато тих же проблем, що і "статичне" ключове слово 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

Це може бути складним, і, ймовірно, варіюється від мови до мови, але в Луї здається, що кожного разу, коли виконується функція, її стан скидається. Я говорю це тому, що результати з наведеного вище коду були б іншими, якби ми отримували доступ до myclosureфункції / стану безпосередньо (замість анонімної функції вона повертається), як pvalueбуло б скинуто назад до 10; але якщо ми отримаємо доступ до стану мого закриття через x (анонімна функція), ви можете побачити, що pvalueвоно живе і добре десь у пам'яті. Я підозрюю, що в цьому є трохи більше, можливо, хтось може краще пояснити природу реалізації.

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

Що я не впевнений у тому, що в прикладі коду вище, чи вважається анонімна функція або функція вищого порядку закриттям?


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. Діаграма ланцюга, збережена для цієї внутрішньої функції, є діючою ланцюгом дії. Діючий чинний ланцюг сфери дії - це f1"s". Отже f2«s сфера ланцюг 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 тощо.


-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 створюють закриття майже однаково.


Ця відповідь є CW, тому що я не заслуговую балів за чудову відповідь Еріка. Будь ласка, підсиліть її, як вважаєте за потрібне. HTH
goodguys_activate

3
-1: Ваше пояснення занадто кореневе в C #. Закриття використовується в багатьох мовах і набагато більше, ніж синтаксичний цукор у цих мовах і охоплює як функцію, так і стан.
Мартін Йорк

1
Ні, закриття - це не просто "оптимізація компілятора", ані синтаксичний цукор. -1
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.