Замыкание (программирование)
Из Википедии, бесплатной энциклопедии
Замыкание (англ. closure) в программировании — это различные методики передачи в функцию первого класса (она же callback) информации о переменных вне её тела[1]. Другими словами, замыкание состоит из 1) ссылки на функцию, и 2) пакета данных, показывающего, при каких обстоятельствах функцию вызвали — и наоборот, функция может модифицировать этот пакет, чтобы передать наружу, что случилось при её исполнении. Простейший пример: операция сортировки массива принимает функцию сравнения, в эту функцию окольным путём передаётся, по какому полю сортировать, и тем же окольным путём функция накапливает статистику, сколько было сравнений.
Замыкание, так же как и экземпляр объекта, есть способ представления функциональности и данных, связанных и упакованных вместе.
Существуют различные методики создания замыканий:
- Глобальная переменная, с той скидкой, что теряется реентерабельность — стандартная библиотека Си вроде
qsort
. - Дополнительный параметр, который гарантированно передаётся неизменным и позволяет упаковать небольшой объект или передать по ссылке большой — низкоуровневые библиотеки наподобие WinAPI[2];
- Объект с операцией «вызов», создаваемый каждый раз,— Си++, Java. Такой объект также может перехватывать локальные переменные и/или ссылаться на текущий объект
this
, связывая внутреннюю функцию с местом, где она определена в коде.- Чтобы разнотипные объекты можно было однообразно вызывать, существуют несколько разных подходов: они поддерживают один и тот же интерфейс (Java), метапрограммирование (немалая часть Си++ вроде
std::sort
), промежуточный объектstd::function
и более лёгкие вродеfunction_ref
, берущие тонкости типа на себя (тоже Си++).
- Чтобы разнотипные объекты можно было однообразно вызывать, существуют несколько разных подходов: они поддерживают один и тот же интерфейс (Java), метапрограммирование (немалая часть Си++ вроде
- Нужные данные автоматически содержатся в любой ссылке на функцию — PHP, JavaScript[1], Ruby.
- Метод объекта вместе с указателем на сам объект — Delphi.
Некоторые из назначений
[править | править код]Основное назначение замыкания (в любом виде) — разделение ответственности между слоями кода: «библиотечная» функция готовит данные, функция с «пользовательского» слоя обрабатывает. Примерные сценарии: при наступлении события вызвать пользовательскую функцию[3], обойти несколько объектов и для каждого вызвать пользовательскую функцию[2], провести сортировку в пользовательском порядке[4]… Чтобы библиотека не теряла общность, «пользовательская» функция обязана иметь нейтральную сигнатуру, не выдающую, какими сторонними данными происходит обмен.
Условный пример: библиотечная функция-генератор пройтиПростыеЧисла
умеет отыскивать среди чисел простые, но ничего с ними не делает и взамен вызывает для обработки пользовательскую функцию-потребителя добавитьВСписок
. (Замыкание сделано дополнительным параметром на манер WinAPI — даже такой примитивной конструкции хватает, чтобы разделить ответственность. Стрелкой ↑ будем обозначать ссылки там, где важна именно такая передача данных.)
функция добавитьВСписок(x : целое, замык : ↑объект) (замык → ↑список).добавь(x) y : список пройтиПростыеЧисла(1000, добавитьВСписок, y)
Такой метод, применяемый в низкоуровневых библиотеках на манер WinAPI, не защищён от дурака: если программист ошибётся с преобразованием типа и напишет замык → ↑кнопка
, будет порча памяти или аварийный останов. Потому желательна языковая поддержка замыканий, заодно решающая и новые задачи:
- Программирование в структурном стиле — внешний вызов и внутреннее тело напоминают составной оператор. В частности, генераторы данных и итераторы[3].
y : список пройтиПростыеЧисла(1000, [↑y](x : целое) → y.добавь(x) )
- Такая конструкция отличается от оператора
for
лишь тем, что внутри тело функции, а не тело цикла. Аналогомcontinue
будетreturn
, а аналогаbreak
в общем случае нет.
- Программирование в функциональном стиле — функции первого класса, карринг, композиция функций[3].
функция форматироватьВалюту(валюта : строка, точность : целое) вернуть [валюта, точность](x : дробное) → форматировать(x, точность) + " " + валюта форматироватьЕвро := форматироватьВалюту("€", 2) // получаем объект-функцию дробное → строка
- Один из способов сделать скрытые (доступные только объекту-владельцу) данные, не пересекающиеся ни с другими объектами, ни с глобальными переменными[3].
функция сделатьСчётчик() n := 0 вернуть [изменяемая n]() → n++ // объект-функция без параметров возвращает числа 0, 1, 2…, // даже после того, как локальная переменная n исчезнет
- Принцип действует и в Си++, и в JavaScript/Ruby, но несколько по-разному: в Си++ переменная
n
, перехваченная по копии, находится внутри объекта и никак не связана с локальнойn
. JavaScript и Ruby перехватывают по ссылке, но запоминают контекст, откуда была создана функция, и сколько функций, столько контекстов[5].
- Для принципа «не повторяйся». В Си нередко повторяющийся код оформляют как макрос препроцессора, потому что иначе трудоёмко перехватить локальные переменные. Замыкания снимают всю трудоёмкость:
s := "alpha,bravo,charlie" начало := 0 сброс := [s, ↑начало](конец : целое) → напечатать s[начало..конец) // или иным образом обработать кусок строки цикл i := [0..|s|) если s[i] = ',' сброс(i) начало := i + 1 сброс(|s|)
- Чтобы оформить небольшой объект как функцию — для кэширования предыдущих вызовов, фильтрации ввода и другого[3].
Примеры
[править | править код]Больше примеров смотрите в викиучебнике.
В языке Scheme
[править | править код](define (make-adder n) ; возвращает замкнутое лямбда-выражение (lambda (x) ; в котором x - связанная переменная, (+ x n) ; а n - свободная (захваченная из внешнего контекста) ) ) (define add1 (make-adder 1)) ; делаем процедуру для прибавления 1 (add1 10) ; вызываем её, возвращает 11 (define sub1 (make-adder -1)); делаем процедуру для вычитания 1 (sub1 10) ; вызываем её, возвращает 9
В языке JavaScript[6]
[править | править код]'use strict'; const add = function(x) { return function(y) { const z = x + y; console.log(x + '+' + y + '=' + z); return z; }; }; const res = add(3)(6); // вернёт 9 и выведет в консоль 3+6=9 console.log(res);
Этот же код в версии ECMAScript2015 с использованием «стрелочных функций»:
'use strict'; const add = x => y => { const z = x + y; console.log(x + '+' + y + '=' + z); return z; }; const res = add(3)(6); // вернёт 9 и выведет в консоль 3+6=9 console.log(res);
Пояснение: в JavaScript сочетание => является оператором объявления стрелочной функции, см например https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Functions/Arrow_functions. Здесь в константу add помещается функция от аргумента x, результатом которой будет являться другая функция, а именно функция от аргумента y, результат которой вычисляется приведённым в фигурных скобках блоком кода. Этот блок кода опирается на аргумент y своей функции и на замыкание, создаваемое для аргумента x внешней функции.
При вызове add(3)(6) функция, хранящаяся в переменной add, вызывается с аргументом 3 и возвращает функцию, завязанную на значение 3 в замыкании x.
Далее в рамках такого обращения эта функция выполняется с аргументом y = 6 и возвращает 9.
Можно сделать рекурсивное замыкание:
'use strict'; const add = x => y => { const z = x + y; console.log(x + '+' + y + '=' + z); return add(z); }; const res = add(1)(4)(6)(9); console.log(res); /* 1+4=5 5+6=11 11+9=20 [Function]*/
Когда JS-код работает, локальные переменные хранятся в scope. В JavaScript локальные переменные могут оставаться в памяти даже после того, как функция вернула значение.
Все функции в JavaScript — это замыкания, то есть всегда, когда создается функция, создается замыкание, хоть и зачастую оно пустое, так как функции обычно из объявления контекста, как правило, ничего не используют. Но нужно понимать разницу между созданием замыкания и созданием нового scope-объекта: замыкание (функция + ссылка на текущую цепочку scope-объектов) создается при определении функции, но новый scope-объект создается (и используется для модификации цепочки scope-объектов замыкания) при каждом вызове функции.
В языке PHP
[править | править код]В PHP замыкания — это анонимные функции, особые конструкции, которые позволяют описывать функции, не имеющие определённых имён.
<?php function add($x) { return function ($y) use ($x) { // <-- анонимная функция (замыкание) return $x + $y; }; // <-- эта точка с запятой здесь нужна! } echo add(3)(5) . PHP_EOL; // Выведет: 8 $f = add(3); var_dump($f); // Выведет: object(Closure) echo $f(6) . PHP_EOL; // Выведет: 9
В PHP наследование переменных из родительской области видимости осуществляется с помощью конструкции use путем явного указания имен наследуемых переменных.
Другой пример с передачей замыкания в метод, где ожидается callable-параметр:
<?php function power($arr, $exp) { // переменная $func будет хранить ссылку на объект класса Closure, который описывает наше замыкание $func = function ($el) use ($exp) { return $el ** $exp; }; return array_map($func, $arr); } $list = [1, 3, 4]; var_dump(power($list, 2)); // Выведет: array(3) {[0]=>int(1) [1]=>int(9) [2]=>int(16)} var_dump(power($list, 3)); // Выведет: array(3) {[0]=>int(1) [1]=>int(27) [2]=>int(64)}
См. также
[править | править код]Примечания
[править | править код]- ↑ 1 2 Closures — JavaScript | MDN . Дата обращения: 17 сентября 2024. Архивировано 18 сентября 2024 года.
- ↑ 1 2 Функция EnumWindows (winuser.h) - Win32 apps | Microsoft Learn . Дата обращения: 5 октября 2024. Архивировано 7 октября 2024 года.
- ↑ 1 2 3 4 5 Источник . Дата обращения: 18 сентября 2024. Архивировано 18 сентября 2024 года.
- ↑ std::sort - cppreference.com . Дата обращения: 5 октября 2024. Архивировано 7 октября 2024 года.
- ↑ Blocks Can Be Closures — Containers, Blocks, and Iterators — Programming Ruby. The Pragmatic Programmer’s Guide. Дата обращения: 29 сентября 2011. Архивировано 23 сентября 2011 года.
- ↑ Closure: Function closures and storing data in function scope. — 2018-01-08. Архивировано 29 ноября 2019 года.
![]() | В статье не хватает ссылок на источники (см. рекомендации по поиску). |
![]() | В другом языковом разделе есть более полная статья Closure (computer programming) (англ.). |