stringtranslate.com

Замыкание (компьютерное программирование)

В языках программирования замыкание , а также лексическое замыкание или замыкание функции , представляет собой метод реализации привязки имени с лексической областью действия на языке с первоклассными функциями . С функциональной точки зрения замыкание — это запись , хранящая функцию [a] вместе со средой. [1] Среда — это сопоставление, связывающее каждую свободную переменную функции (переменные, которые используются локально, но определены в охватывающей области) со значением или ссылкой , к которой было привязано имя при создании замыкания. [b] В отличие от простой функции, замыкание позволяет функции получать доступ к этим захваченным переменным через копии замыкания их значений или ссылок, даже если функция вызывается вне ее области действия.

История и этимология

Концепция замыканий была разработана в 1960-х годах для механической оценки выражений в λ-исчислении и впервые была полностью реализована в 1970 году как языковая функция языка программирования PAL для поддержки первоклассных функций с лексической областью видимости . [2]

Питер Ландин определил термин «замыкание» в 1964 году как наличие части среды и части управления , используемых его машиной SECD для оценки выражений. [3] Джоэл Мозес приписывает Ландину введение термина «замыкание» для обозначения лямбда-выражения с открытыми привязками (свободными переменными), которые были закрыты (или связаны) лексической средой, что привело к закрытому выражению или замыканию. [4] [5] Это использование впоследствии было принято Сассманом и Стилом , когда они определили Scheme в 1975 году, [6] вариант Lisp с лексической областью действия , и получило широкое распространение.

Сассман и Абельсон также использовали термин «замыкание» в 1980-х годах со вторым, несвязанным значением: свойство оператора, который добавляет данные в структуру данных , иметь возможность добавлять вложенные структуры данных. Такое использование термина происходит от использования математики , а не от предыдущего использования в информатике. Такое совпадение в терминологии авторы считают «неудачным». [7]

Анонимные функции

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

Например, в следующем коде Python :

def  f ( x ):  def  g ( y ):  return  x  +  y  return  g  # Возвращает замыкание.def  h ( x ):  return  лямбда  y :  x  +  y  # Вернуть замыкание.# Назначение конкретных замыканий переменным. а  знак равно  ж ( 1 ) б  знак равно  час ( 1 )# Использование замыканий, хранящихся в переменных. утверждать  a ( 5 )  ==  6 утверждать  b ( 5 )  ==  6# Использование замыканий без предварительной привязки их к переменным. утверждать, что  f ( 1 )( 5 )  ==  6  # f(1) — замыкание. утверждать, что  h ( 1 )( 5 )  ==  6  # h(1) — замыкание.

значения aи bявляются замыканиями, в обоих случаях создаваемыми путем возврата вложенной функции со свободной переменной из включающей функции, так что свободная переменная привязывается к значению параметра xвключающей функции. Затворы aи bфункционально идентичны. Единственная разница в реализации состоит в том, что в первом случае мы использовали вложенную функцию с именем , gа во втором случае мы использовали анонимную вложенную функцию (используя ключевое слово Python lambdaдля создания анонимной функции). Исходное имя, если таковое имеется, использованное при их определении, не имеет значения.

Замыкание — это такое же значение, как и любое другое значение. Его не обязательно присваивать переменной, его можно использовать напрямую, как показано в последних двух строках примера. Такое использование можно считать «анонимным закрытием».

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

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

x  =  1 числа  =  [ 1 ,  2 ,  3 ]def  f ( y ):  вернуть  x  +  yкарта ( f ,  числа ) карта ( лямбда  y :  x  +  y ,  числа )

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

Этого также можно достичь с помощью затенения переменных (что уменьшает область действия нелокальной переменной), хотя на практике это менее распространено, поскольку оно менее полезно и затенение не рекомендуется. В этом примере fможно увидеть замыкание, поскольку xтело fпривязано к xглобальному пространству имен, а не xк локальному g:

х  =  0def  f ( y ):  вернуть  x  +  ydef  g ( z ):  x  =  1  # локальный x тени глобальный x  return  f ( z )g ( 1 )  # оценивается как 1, а не 2

Приложения

Использование замыканий связано с языками, где функции являются объектами первого класса , в которых функции могут быть возвращены как результаты функций более высокого порядка или переданы в качестве аргументов для вызовов других функций; если функции со свободными переменными являются первоклассными, то возврат одной из них создает замыкание. Сюда входят языки функционального программирования , такие как Lisp и ML , а также многие современные мультипарадигмальные языки, такие как Julia , Python и Rust . Замыкания также часто используются с обратными вызовами , особенно для обработчиков событий , например, в JavaScript , где они используются для взаимодействия с динамической веб-страницей .

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

Первоклассные функции

Замыкания обычно появляются в языках с функциями первого класса — другими словами, такие языки позволяют передавать функции в качестве аргументов, возвращать их из вызовов функций, привязывать к именам переменных и т. д., точно так же, как и более простые типы, такие как строки и целые числа. Например, рассмотрим следующую функцию Scheme :

; Верните список всех книг, проданных хотя бы ПОРОГОВЫХ экземпляров. ( определить ( порог самых продаваемых книг ) ( фильтр ( лямбда ( книга ) ( >= ( книга продаж книг )) порог )) список книг ))          

В этом примере лямбда-выражение (lambda (book) (>= (book-sales book) threshold)) появляется внутри функции best-selling-books. Когда вычисляется лямбда-выражение, Scheme создает замыкание, состоящее из кода лямбда-выражения и ссылки на переменную threshold, которая является свободной переменной внутри лямбда-выражения.

Затем замыкание передается filterфункции, которая вызывает ее повторно, чтобы определить, какие книги следует добавить в список результатов, а какие следует удалить. Поскольку замыкание имеет ссылку на threshold, оно может использовать эту переменную каждый раз, когда filterего вызывает. Функция filterможет быть определена в отдельном файле.

Вот тот же пример, переписанный на JavaScript , другом популярном языке с поддержкой замыканий:

// Возвращаем список всех книг, проданных хотя бы «пороговых» копий. функция bestSellingBooks ( порог ) { return bookList . фильтр ( книга => книга . продажи >= порог ); }        

Оператор стрелки =>используется для определения выражения стрелочной функции и Array.filterметода [8] вместо глобальной filterфункции, но в остальном структура и эффект кода такие же.

Функция может создать замыкание и вернуть его, как в этом примере:

// Возвращаем функцию, которая аппроксимирует производную f // с использованием интервала dx, который должен быть соответствующим образом малым. производная функции ( f , dx ) { return x => ( f ( x + dx ) - f ( x )) / dx ; }             

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

Государственное представительство

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

Таким образом, в языках с состоянием замыкания могут использоваться для реализации парадигм представления состояния и сокрытия информации , поскольку upvalue замыкания (его закрытые переменные) имеют неопределенный размер , поэтому значение, установленное в одном вызове, остается доступным в следующем. Замыкания, используемые таким образом, больше не имеют ссылочной прозрачности и, таким образом, больше не являются чистыми функциями ; тем не менее, они обычно используются в нечистых функциональных языках, таких как Scheme .

Другое использование

Замыкания имеют множество применений:

( определить foo #f ) ( определить бар #f )    ( let (( secret-message "none" )) ( set! foo ( lambda ( msg ) ( set! secret-message msg ))) ( set! bar ( lambda () secret-message )))              ( дисплей ( бар ) ) ; печатает «none» ( newline ) ( foo «встретимся у доков в полночь» ) ( display ( bar )) ; печатает «встретимся в доках в полночь»     

Примечание. Некоторые ораторы называют любую структуру данных, которая связывает лексическое окружение, замыканием, но этот термин обычно относится конкретно к функциям.

Реализация и теория

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

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

Это объясняет, почему обычно языки, которые изначально поддерживают замыкания, также используют сбор мусора . Альтернативой является ручное управление памятью нелокальных переменных (явное выделение памяти в куче и освобождение после завершения) или, при использовании выделения стека, принятие языком того, что определенные варианты использования приведут к неопределенному поведению из-за висячих указателей на освобожденные автоматические переменные, как в лямбда-выражениях в C++11 [10] или вложенных функциях в GNU C. [11] Проблема funarg (или проблема «функционального аргумента») описывает сложность реализации функций как объектов первого класса в стеке. - основанный на языке программирования, такой как C или C++. Аналогично в версии D 1 предполагается, что программист знает, что делать с делегатами и автоматическими локальными переменными, поскольку их ссылки будут недействительны после возврата из области определения (автоматические локальные переменные находятся в стеке) – это по-прежнему позволяет использовать много полезных функциональные шаблоны, но для сложных случаев требуется явное выделение кучи для переменных. Версия D 2 решила эту проблему, определяя, какие переменные должны храниться в куче, и выполняла автоматическое распределение. Поскольку D использует сбор мусора, в обеих версиях нет необходимости отслеживать использование переменных по мере их передачи.

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

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

Scheme , который имеет систему лексической области видимости, подобную ALGOL , с динамическими переменными и сборкой мусора, не имеет модели стекового программирования и не страдает от ограничений языков, основанных на стеке. Замыкания естественным образом выражаются в Scheme. Лямбда-форма заключает в себе код, а свободные переменные ее среды сохраняются в программе до тех пор, пока к ним возможен доступ, и поэтому их можно использовать так же свободно, как и любое другое выражение схемы. [ нужна цитата ]

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

Замыкания тесно связаны с функциональными объектами ; переход от первого ко второму известен как дефункционализация или лямбда-лифтинг ; см. также преобразование замыкания . [ нужна цитата ]

Различия в семантике

Лексическое окружение

Поскольку в разных языках не всегда имеется общее определение лексической среды, их определения замыкания также могут различаться. Общепринятое минималистское определение лексической среды определяет ее как набор всех привязок переменных в области видимости, и это также то, что замыкания в любом языке должны фиксировать. Однако значение привязки переменной также различается. В императивных языках переменные привязываются к относительным местам в памяти, в которых могут храниться значения. Хотя относительное расположение привязки не меняется во время выполнения, значение в привязанном местоположении может измениться. В таких языках, поскольку замыкание фиксирует привязку, любая операция над переменной, независимо от того, выполняется ли оно из замыкания или нет, выполняется в одной и той же относительной ячейке памяти. Это часто называют захватом переменной «по ссылке». Вот пример, иллюстрирующий концепцию ECMAScript , который является одним из таких языков:

// Javascript var f , g ; функция foo () { var x ; е = функция () { возвращение ++ х ; }; г = функция () { возвращение -- х ; }; х = 1 ; alert ( 'внутри foo, вызовите f(): ' + f ()); } Фу (); // 2 оповещения ( 'вызов g(): ' + g ()); // 1 (--x) оповещение ( 'вызов g(): ' + g ()); // 0 (--x) alert ( 'вызов f(): ' + f ()); // 1 (++x) оповещение ( 'вызов f(): ' + f ()); // 2 (++x)                                       

Функция fooи замыкания, на которые ссылаются переменные, fиспользуют gодну и ту же относительную ячейку памяти, обозначенную локальной переменной x.

В некоторых случаях описанное выше поведение может быть нежелательным, и необходимо связать другое лексическое замыкание. Опять же, в ECMAScript это будет сделано с использованием метода Function.bind().

Пример 1: Ссылка на несвязанную переменную

[13]

var модуль = { x : 42 , getX : function () { верните это . Икс ; } } вар unboundGetX = модуль . получитьХ ; консоль . журнал ( unboundGetX ()); // Функция вызывается в глобальной области // выдает неопределенное значение, поскольку 'x' не указан в глобальной области.              варboundGetX = unboundGetX . _ привязать ( модуль ); // указываем объектный модуль в качестве консоли закрытия . журнал ( boundGetX ()); // выдает 42     

Пример 2: Случайная ссылка на связанную переменную

В этом примере ожидаемое поведение будет заключаться в том, что каждая ссылка должна выдавать свой идентификатор при нажатии; но поскольку переменная «e» привязана к указанной выше области и лениво оценивается при щелчке, на самом деле происходит то, что каждое событие щелчка выдает идентификатор последнего элемента в «elements», привязанный в конце цикла for. [14]

вар элементы = документ . getElementsByTagName ( 'a' ); // Неправильно: e привязана к функции, содержащей цикл for, а не к замыканию "дескриптора" for ( var e of elements ) { e . onclick = дескриптор функции () { alert ( e . id ); } }                  

Опять же, здесь переменная eдолжна быть связана с областью использования блока handle.bind(this)или letключевым словом.

С другой стороны, многие функциональные языки, такие как ML , привязывают переменные непосредственно к значениям. В этом случае, поскольку нет возможности изменить значение переменной после ее привязки, нет необходимости разделять состояние между замыканиями — они просто используют одни и те же значения. Это часто называют захватом переменной «по значению». Локальные и анонимные классы Java также попадают в эту категорию — они требуют, чтобы захваченные локальные переменные были final, что также означает, что нет необходимости делиться состоянием.

Некоторые языки позволяют выбирать между сохранением значения переменной или ее местоположения. Например, в C++11 захваченные переменные объявляются либо с помощью [&], что означает захват по ссылке, либо с помощью [=], что означает захват по значению.

Еще одно подмножество — ленивые функциональные языки, такие как Haskell , привязывают переменные к результатам будущих вычислений, а не к значениям. Рассмотрим этот пример на Haskell:

-- Haskell foo :: Дробное a => a -> a -> ( a -> a ) foo x y = ( \ z -> z + r ) где r = x / y                         f :: Дробное a => a -> a f = foo 1 0           основной = печать ( f 123 )    

Привязка rзахваченного замыканием, определенного внутри функции, fooсвязана с вычислением (x / y), которое в данном случае приводит к делению на ноль. Однако, поскольку фиксируется именно вычисление, а не значение, ошибка проявляется только при вызове замыкания, а затем при попытке использовать захваченную привязку.

Закрытие выезда

Еще больше различий проявляется в поведении других конструкций с лексической областью действия, таких как returnи breakоператоры continue. Такие конструкции, как правило, можно рассматривать с точки зрения вызова escape-продолжения , установленного включающим оператором управления (в случае breakи continueтакая интерпретация требует, чтобы циклические конструкции рассматривались с точки зрения рекурсивных вызовов функций). В некоторых языках, таких как ECMAScript, returnэто относится к продолжению, установленному замыканием, которое лексически является самым внутренним по отношению к оператору — таким образом, внутри returnзамыкания передается управление коду, который его вызвал. Однако в Smalltalk внешне похожий оператор ^вызывает escape-продолжение, установленное для вызова метода, игнорируя escape-продолжения любых промежуточных вложенных замыканий. Продолжение escape конкретного замыкания может быть вызвано в Smalltalk только неявно, достигнув конца кода замыкания. Эти примеры в ECMAScript и Smalltalk подчеркивают разницу:

"Светская беседа" фу  | хз |  хз  :=  #( 1  2  3  4 ) .  хз  делаю: [ : х  |  ^ х ] .  ^ 0 бар Показ  расшифровки  : ( self  foo  printString ) "печатает 1"
// Функция ECMAScript foo () { var xs = [ 1 , 2 , 3 , 4 ]; хз . forEach ( функция ( х ) { возврат х ; }); вернуть 0 ; } Оповещение ( фу ()); // печатает 0                  

Приведенные выше фрагменты кода будут вести себя по-разному, поскольку ^операторы Smalltalk и оператор JavaScript returnне аналогичны. В примере ECMAScript return xоставит внутреннее замыкание, чтобы начать новую итерацию цикла forEach, тогда как в примере Smalltalk ^xпрервет цикл и вернется из метода foo.

Common Lisp предоставляет конструкцию, которая может выражать любое из вышеперечисленных действий: Lisp (return-from foo x)ведет себя как Smalltalk ^x , а Lisp (return-from nil x)ведет себя как JavaScript return x . Следовательно, Smalltalk позволяет захваченному escape-продолжению пережить тот период, в котором оно может быть успешно вызвано. Учитывать:

«Светская беседа» foo  ^ [ : x  |  ^ х ] бар  |  ж  |  f  :=  сам  фу . значение  f  :  123  «ошибка!»

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

Некоторые языки, такие как Ruby , позволяют программисту выбирать способ returnзахвата. Пример в Ruby:

# Рубин# Замыкание с использованием процедуры def foo f = Proc . new { return «возврат из foo изнутри процедуры» } f . call # управление оставляет здесь foo return "return from foo" end            # Замыкание с использованием лямбда- def bar f = лямбда { return "return from лямбда" } f . вызов # управление не покидает бар здесь return "возврат из бара" end            puts foo # печатает "возврат из foo изнутри proc" puts bar # печатает "возврат из bar"    

Оба Proc.newи lambdaв этом примере являются способами создания замыкания, но семантика созданных таким образом замыканий различна в зависимости от оператора return.

В Scheme определение и область действия returnоператора управления являются явными (и только для примера условно названы «return»). Ниже приведен прямой перевод примера Ruby.

; Схема ( определить вызов/копию вызова с текущим продолжением )  ( define ( foo ) ( call/cc ( лямбда ( return ) ( define ( f ) ( return "возврат из foo изнутри proc" )) ( f ) ; управление оставляет foo здесь ( return "возврат из foo" ))))            ( define ( bar ) ( call/cc ( лямбда ( return ) ( define ( f ) ( call/cc ( лямбда ( return ) ( return «возврат из лямбда» )))) ( f ) ; управление не покидает bar здесь ( return "возвращение из бара" ))))               ( дисплей ( фу )) ; печатает «возврат из foo изнутри proc» ( новая строка ) ( display ( bar )) ; печатает «возврат из бара»    

Замыкающие конструкции

В некоторых языках есть функции, имитирующие поведение замыканий. В таких языках, как C++ , C# , D , Java , Objective-C и Visual Basic (.NET) (VB.NET), эти функции являются результатом объектно-ориентированной парадигмы языка.

Обратные вызовы (С)

Некоторые библиотеки C поддерживают обратные вызовы . Иногда это реализуется путем предоставления двух значений при регистрации обратного вызова в библиотеке: указатель на функцию и отдельный void*указатель на произвольные данные по выбору пользователя. Когда библиотека выполняет функцию обратного вызова, она передает указатель данных. Это позволяет обратному вызову сохранять состояние и ссылаться на информацию, полученную в момент его регистрации в библиотеке. Эта идиома похожа на замыкания по функциональности, но не по синтаксису. Указатель void*не является типобезопасным , поэтому эта идиома C отличается от типобезопасных замыканий в C#, Haskell или ML.

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

Вложенная функция и указатель функции (C)

С расширением GNU Compiler Collection (GCC) можно использовать вложенную функцию [15] , а указатель функции может эмулировать замыкания, при условии, что функция не выходит из содержащейся области. Следующий пример недействителен, поскольку adderявляется определением верхнего уровня (в зависимости от версии компилятора он может дать правильный результат, если скомпилировать его без оптимизации, т. е. в -O0):

#include <stdio.h> typedef int ( * fn_int_to_int )( int ); // тип функции int->int   fn_int_to_int adder ( int число ) { int add ( int value ) { возвращаемое значение + число ; } Вернуться и добавить ; // Оператор & здесь необязателен, поскольку имя функции в C — это указатель, указывающий на саму себя }                int main ( void ) { fn_int_to_int add10 = adder ( 10 ); printf ( "%d \n " , add10 ( 1 )); вернуть 0 ; }          

Но перемещение adder(и, необязательно, typedef) mainделает его действительным:

#include <stdio.h> int main ( void ) { typedef int ( * fn_int_to_int ) ( int ); // тип функции int->int fn_int_to_int adder ( int number ) { int add ( int value ) { возвращаемое значение + число ; } Вернуться добавить ; } fn_int_to_int add10 = сумматор ( 10 ); printf ( "%d \n " , add10 ( 1 )); вернуть 0 ; }                                 

Если это выполнено, теперь печатается 11как положено.

Локальные классы и лямбда-функции (Java)

Java позволяет определять классы внутри методов . Они называются локальными классами . Если такие классы не названы, они называются анонимными классами (или анонимными внутренними классами). Локальный класс (именованный или анонимный) может ссылаться на имена в лексически включающих классах или на переменные только для чтения (отмеченные как final) в лексически включающем методе.

класс  CalculationWindow расширяет JFrame { частный изменчивый результат int ; // ... public void CalculateInSeparateThread ( Final URI uri ) { // Выражение «new Runnable() { ... }» — это анонимный класс, реализующий интерфейс Runnable. new Thread ( new Runnable () { void run () { // Он может читать конечные локальные переменные: Calculate ( uri ); // Он может получить доступ к частным полям включающего класса: result = result + 10 ; } } ). начинать (); } }                                   

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

Захват переменных по ссылке можно эмулировать, используя finalссылку на изменяемый контейнер, например одноэлементный массив. Локальный класс не сможет изменить значение ссылки на контейнер, но сможет изменить содержимое контейнера.

С появлением лямбда-выражений в Java 8 [16] замыкание приводит к выполнению приведенного выше кода как:

класс  CalculationWindow расширяет JFrame { частный изменчивый результат int ; // ... public void CalculateInSeparateThread ( final URI uri ) { // Код () -> { /* code */ } является замыканием. новый поток (() -> { вычислить ( uri ); результат = результат + 10 ; }). начинать (); } }                           

Локальные классы — это один из типов внутренних классов , которые объявляются в теле метода. Java также поддерживает внутренние классы, объявленные как нестатические члены включающего класса. [17] Их обычно называют просто «внутренними классами». [18] Они определены в теле включающего класса и имеют полный доступ к переменным экземпляра включающего класса. Из-за их привязки к этим переменным экземпляра экземпляр внутреннего класса может быть создан только с явной привязкой к экземпляру включающего класса с использованием специального синтаксиса. [19]

public class EnclosingClass { /* Определить внутренний класс */ public class InnerClass { public int ignoreAndReturnCounter () { return counter ++ ; } }                 частный счетчик int ; { счетчик = 0 ; }        public int getCounter () { возврат счетчика ; }       public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = новый EnclosingClass (); /* Создать экземпляр внутреннего класса с привязкой к экземпляру */ EnclosingClass . InnerClass InternalClassInstance = EnclosingClassInstance . новый ВнутреннийКласс ();                 for ( int i = enclosingClassInstance . getCounter (); ( i = InnerClassInstance . инкрементАнкретурнкаунтер ()) < 10 ; /* шаг приращения опущен */ ) { System . вне . печатьln ( я ); } } }              

При выполнении будут выведены целые числа от 0 до 9. Будьте осторожны, чтобы не путать этот тип класса с вложенным классом, который объявляется таким же образом с сопутствующим использованием модификатора «static»; они не дают желаемого эффекта, а представляют собой просто классы без специальной привязки, определенной во включающем классе.

Начиная с Java 8 , Java поддерживает функции как объекты первого класса. Лямбда-выражения этой формы считаются типом, Function<T,U>где T — это домен, а U — тип изображения. Выражение можно вызвать с помощью его .apply(T t)метода, но не с помощью вызова стандартного метода.

public static void main ( String [] args ) { Function < String , Integer > length = s -> s . длина ();             Система . вне . println ( length . apply ( "Привет, мир!" ) ); // Напечатаем 13. }   

Блоки (C, C++, Objective-C 2.0)

Apple представила блоки , форму замыкания, как нестандартное расширение в C , C++ , Objective-C 2.0 и в Mac OS X 10.6 «Snow Leopard» и iOS 4.0 . Apple сделала свою реализацию доступной для компиляторов GCC и clang.

Указатели на блок и блочные литералы отмечены значком ^. Обычные локальные переменные фиксируются по значению при создании блока и доступны только для чтения внутри блока. Переменные, которые необходимо захватить по ссылке, отмечены значком __block. Блоки, которые должны сохраняться за пределами области, в которой они созданы, возможно, придется скопировать. [20] [21]

typedef int ( ^ IntBlock )();  IntBlock downCounter ( int start ) { __block int i = start ; return [[ ^ int () { return i -- ; } копировать ] автовыпуск ]; }                 IntBlock f = downCounter ( 5 ); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ());      

Делегаты (C#, VB.NET, D)

Анонимные методы C# и лямбда-выражения поддерживают замыкание:

вар данные = новый [] { 1 , 2 , 3 , 4 }; вар множитель = 2 ; вар результат = данные . Выберите ( x => x * множитель );                 

Visual Basic .NET , который имеет множество языковых функций, аналогичных функциям C#, также поддерживает лямбда-выражения с замыканиями:

Тусклые данные = { 1 , 2 , 3 , 4 } Тусклый множитель = 2 Тусклый результат = данные . Выберите ( Функция ( x ) x * множитель )               

В D замыкания реализуются делегатами, указателем функции в паре с указателем контекста (например, экземпляром класса или кадром стека в куче в случае замыканий).

автоматический тест1 () { int a = 7 ; вернуть делегата ( ) { вернуть + 3 ; }; // анонимная конструкция делегата }               автоматический тест2 () { int a = 20 ; int foo () { return a + 5 ; } // внутренняя функция return & foo ; // другой способ создания делегата }                  void bar () { auto dg = test1 (); дг (); // =10 // ок, test1.a закрыт и все еще существует         дг = тест2 (); дг (); // =25 // ок, test2.a закрыт и все еще существует }    

Версия D 1 имеет ограниченную поддержку замыканий. Например, приведенный выше код не будет работать корректно, поскольку переменная a находится в стеке, и после возврата из test() ее больше нельзя использовать (вероятнее всего, вызов foo через dg() вернет ' случайное целое число). Эту проблему можно решить, явно выделив переменную «a» в куче или используя структуры или классы для хранения всех необходимых закрытых переменных и создав делегат из метода, реализующего тот же код. Замыкания можно передавать другим функциям, если они используются только тогда, когда значения, на которые они ссылаются, действительны (например, вызов другой функции с замыканием в качестве параметра обратного вызова) и полезны для написания общего кода обработки данных, поэтому это ограничение На практике зачастую это не является проблемой.

Это ограничение было исправлено в версии D 2 — переменная «a» будет автоматически выделена в куче, поскольку она используется во внутренней функции, и делегат этой функции может выйти из текущей области видимости (через присвоение dg или возврат). Любые другие локальные переменные (или аргументы), на которые не ссылаются делегаты или на которые ссылаются только делегаты, не выходящие за пределы текущей области, остаются в стеке, что проще и быстрее, чем выделение кучи. То же самое справедливо и для методов класса Internal, которые ссылаются на переменные функции.

Функциональные объекты (C++)

C++ позволяет определять объекты функций путем перегрузки operator(). Эти объекты ведут себя как функции функционального языка программирования. Они могут создаваться во время выполнения и могут содержать состояние, но они не фиксируют локальные переменные неявно, как это делают замыкания. Начиная с версии 2011 года , язык C++ также поддерживает замыкания, которые представляют собой тип функционального объекта, автоматически создаваемого из специальной языковой конструкции, называемой лямбда-выражением . Замыкание C++ может захватывать свой контекст либо путем сохранения копий переменных, к которым осуществляется доступ, как членов объекта замыкания, либо по ссылке. В последнем случае, если объект замыкания выходит за пределы объекта, на который ссылаются, его вызов operator()вызывает неопределенное поведение, поскольку замыкания C++ не продлевают время жизни их контекста.

void foo ( string myname ) { int y ; вектор < строка > n ; // ... auto i = std :: find_if ( n.begin ( ), n.end (), // это лямбда-выражение: [ & ] ( const string & s ) { return s ! = myname && s .размер () > y ; } ) ; // 'i' теперь является либо 'n.end()', либо указывает на первую строку в 'n' // которая не равна 'myname' и длина которой больше 'y' }                              

Встроенные агенты (Эйфель)

Eiffel включает в себя встроенные агенты, определяющие замыкания. Встроенный агент — это объект, представляющий процедуру, определяемый путем указания кода процедуры в строке. Например, в

ок_кнопка . клик_событие . подписаться ( агент ( x , y : INTEGER ) сделать карту . Country_at_coordinates ( x , y ). Показать конец )       

аргументом subscribeявляется агент, представляющий процедуру с двумя аргументами; процедура находит страну по соответствующим координатам и отображает ее. Весь агент «подписан» на тип события click_eventдля определенной кнопки, так что всякий раз, когда на этой кнопке возникает экземпляр типа события – потому что пользователь нажал кнопку – процедура будет выполняться с передачей координат мыши как аргументы за xи y.

Основное ограничение агентов Eiffel, которое отличает их от замыканий в других языках, заключается в том, что они не могут ссылаться на локальные переменные из охватывающей области. Это проектное решение помогает избежать двусмысленности при разговоре о значении локальной переменной в замыкании — должно ли это быть последнее значение переменной или значение, полученное при создании агента? Из тела агента можно получить доступ только Current(ссылка на текущий объект, аналогично thisJava), его функциям и аргументам агента. Значения внешних локальных переменных можно передать, предоставив агенту дополнительные закрытые операнды.

C++Builder __closure зарезервированное слово

Embarcadero C++Builder предоставляет резервное слово __closure для предоставления указателя на метод с синтаксисом, аналогичным указателю на функцию. [22]

Стандарт C позволяет писать typedef для указателя на тип функции , используя следующий синтаксис:

typedef void ( * TMyFunctionPointer )( void );    

Аналогичным образом можно объявить typedef для указателя на метод, используя следующий синтаксис:

typedef void ( __closure * TMyMethodPointer )();   

Смотрите также

Примечания

  1. ^ Функция может храниться как ссылка на функцию, например указатель на функцию .
  2. ^ Эти имена обычно относятся к значениям, изменяемым переменным или функциям, но также могут быть другими объектами, такими как константы, типы, классы или метки.

Рекомендации

  1. ^ Сассман и Стил. «Схема: интерпретатор расширенного лямбда-исчисления». «... структура данных, содержащая лямбда-выражение, и среда, которая будет использоваться, когда это лямбда-выражение применяется к аргументам». (Вики-источник)
  2. ^ Тернер, Дэвид А. (2012). «Некоторые истории языков функционального программирования» (PDF) . Международный симпозиум по тенденциям функционального программирования . Конспекты лекций по информатике. Том. 7829. Спрингер. С. 1–20 Утверждение о М-выражениях см. в § 12, § 2, примечание 8. дои : 10.1007/978-3-642-40447-4_1. ISBN 978-3-642-40447-4.
  3. ^ Ландин, П.Дж. (январь 1964 г.). «Механическая оценка выражений» (PDF) . Компьютерный журнал . 6 (4): 308–320. дои : 10.1093/comjnl/6.4.308.
  4. ^ Моисей, Джоэл (июнь 1970 г.). «Функция FUNCTION в LISP, или Почему проблему FUNARG следует называть проблемой среды». Бюллетень ACM Sigsam (15): 13–27. дои : 10.1145/1093410.1093411. hdl : 1721.1/5854. S2CID  17514262. AI Memo 199. Полезной метафорой различия между FUNCTION и QUOTE в LISP является представление QUOTE как пористого или открытого покрытия функции, поскольку свободные переменные уходят в текущую среду. ФУНКЦИЯ действует как закрытое или непористое покрытие (отсюда и термин «закрытие», используемый Ландином). Таким образом, мы говорим об «открытых» лямбда-выражениях (функции в LISP обычно являются лямбда-выражениями) и «закрытых» лямбда-выражениях. [...] Мой интерес к проблеме окружающей среды начался, когда Ландин, глубоко разбиравшийся в этой проблеме, посетил Массачусетский технологический институт в 1966–67 годах. Затем я понял соответствие между списками FUNARG, которые являются результатами оценки «закрытых» лямбда-выражений в LISP и Lambda Closures ISWIM .
  5. ^ Викстрем, Оке (1987). Функциональное программирование с использованием стандартного ML . Прентис Холл. ISBN 0-13-331968-7. Причина, по которой это называется «замыканием», заключается в том, что выражение, содержащее свободные переменные, называется «открытым» выражением, и, связывая с ним привязки его свободных переменных, вы закрываете его.
  6. ^ Сассман, Джеральд Джей ; Стил, Гай Л. младший (декабрь 1975 г.). Схема: интерпретатор расширенного лямбда-исчисления (отчет). Памятка AI 349.
  7. ^ Абельсон, Гарольд ; Сассман, Джеральд Джей ; Сассман, Джули (1996). Структура и интерпретация компьютерных программ. МТИ Пресс. стр. 98–99. ISBN 0-262-51087-1.
  8. ^ "массив.фильтр". Центр разработчиков Mozilla . 10 января 2010 г. Проверено 9 февраля 2010 г.
  9. ^ «Re: FP, OO и отношения. Кто-нибудь превосходит остальных?». 29 декабря 1999 года. Архивировано из оригинала 26 декабря 2008 года . Проверено 23 декабря 2008 г.
  10. ^ Комитет по стандартам лямбда-выражений и замыканий C++. 29 февраля 2008 г.
  11. ^ «6.4 Вложенные функции». Руководство GCC . Если вы попытаетесь вызвать вложенную функцию по ее адресу после выхода из содержащей функции, начнется ад. Если вы попытаетесь вызвать его после выхода из содержащего уровня области видимости и если он ссылается на некоторые переменные, которые больше не входят в область видимости, вам может повезти, но рисковать неразумно. Однако если вложенная функция не ссылается ни на что, выходящее за пределы области видимости, вы можете быть в безопасности.
  12. ^ Основы семантики актеров будут сохраняться. Докторская диссертация по математике в Массачусетском технологическом институте. Июнь 1981 года.
  13. ^ "Функция.prototype.bind()". Веб-документы MDN . Проверено 20 ноября 2018 г.
  14. ^ «Замыкания». Веб-документы MDN . Проверено 20 ноября 2018 г.
  15. ^ «Вложенные функции».
  16. ^ «Лямбда-выражения». Учебники по Java .
  17. ^ «Вложенные, внутренние, члены и классы верхнего уровня». Блог Oracle Джозефа Д. Дарси . Июль 2007 г. Архивировано из оригинала 31 августа 2016 г.
  18. ^ «Пример внутреннего класса». Учебные пособия по Java: Изучение языка Java: классы и объекты .
  19. ^ «Вложенные классы». Учебные пособия по Java: Изучение языка Java: классы и объекты .
  20. ^ «Темы программирования блоков» . Apple Inc., 8 марта 2011 г. Проверено 8 марта 2011 г.
  21. Бенгтссон, Иоахим (7 июля 2010 г.). «Программирование с использованием блоков C на устройствах Apple». Архивировано из оригинала 25 октября 2010 года . Проверено 18 сентября 2010 г.
  22. ^ Полную документацию можно найти по адресу http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure.

Внешние ссылки