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  lambda  y :  x  +  y  # Возвращает замыкание.# Назначение определенных замыканий переменным. a  =  f ( 1 ) b  =  h ( 1 )# Использование замыканий, хранящихся в переменных. assert  a ( 5 )  ==  6 assert  b ( 5 )  ==  6# Использование замыканий без предварительной привязки их к переменным. assert  f ( 1 )( 5 )  ==  6  # f(1) — это замыкание. assert  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 ,  nums ) карта ( лямбда  y :  x  +  y ,  nums )

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

Это также может быть достигнуто с помощью затенения переменных (что уменьшает область действия нелокальной переменной), хотя на практике это встречается реже, так как это менее полезно, а затенение не рекомендуется. В этом примере 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 :

; Возвращает список всех книг, проданных по крайней мере по пороговому количеству экземпляров. ( define ( best-selling-books threshold ) ( filter ( lambda ( book ) ( >= ( book-sales book ) threshold )) book-list ))          

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

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

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

// Возвращает список всех книг, проданных по крайней мере в количестве «пороговых» экземпляров. function bestSellingBooks ( threshold ) { return bookList.filter ( book = > book.sales > = threshold ) ; }        

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

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

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

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

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

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

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

Другие применения

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

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

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

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

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

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

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

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

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

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

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

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

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

Лексическая среда

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

// Javascript var f , g ; function foo () { var x ; f = function () { return ++ x ; }; g = function () { return -- x ; }; x = 1 ; alert ( 'внутри foo, вызов f(): ' + f ()); } foo (); // 2 alert ( 'вызов g(): ' + g ()); // 1 (--x) alert ( 'вызов g(): ' + g ()); // 0 (--x) alert ( 'вызов f(): ' + f ()); // 1 (++x) alert ( 'вызов f(): ' + f ()); // 2 (++x)                                       

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

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

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

[13]

var module = { x : 42 , getX : function () { return this . x ; } } var unboundGetX = module . getX ; console . log ( unboundGetX ()); // Функция вызывается в глобальной области видимости // выдает undefined, так как 'x' не указан в глобальной области видимости.              var boundGetX = unboundGetX . bind ( module ); // указать объект module как замыкание console . log ( boundGetX ()); // выдает 42     

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

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

var elements = document.getElementsByTagName ( 'a' ); // Неправильно: e привязан к функции, содержащей цикл 'for', а не к замыканию " handle " for ( var e of elements ) { e.onclick = function handle ( ) { 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относится к продолжению, установленному замыканием, лексически самым внутренним по отношению к оператору — таким образом, a returnвнутри замыкания передает управление коду, который его вызвал. Однако в Smalltalk внешне похожий оператор ^вызывает escape-продолжение, установленное для вызова метода, игнорируя escape-продолжения любых промежуточных вложенных замыканий. escape-продолжение конкретного замыкания может быть вызвано в Smalltalk только неявно, достигнув конца кода замыкания. Эти примеры на ECMAScript и Smalltalk подчеркивают разницу:

"Smalltalk" foo  | xs |  xs  :=  #( 1  2  3  4 ) .  xs  do: [ : x  |  ^ x ] .  ^ 0 bar  Транскрипт  показать: ( self  foo  printString ) "печатает 1"
// ECMAScript function foo () { var xs = [ 1 , 2 , 3 , 4 ]; xs . forEach ( function ( x ) { return x ; }); return 0 ; } alert ( foo ()); // выводит 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 пережить ту степень, в которой оно может быть успешно вызвано. Рассмотрим:

"Smalltalk" foo  ^ [ : x  |  ^ x ] bar  |  f  |  f  :=  self  foo . Значение  f  :  123  "ошибка!"

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

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

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

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

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

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

Конструкции, подобные замыканию

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

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

Некоторые библиотеки 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 number ) { int add ( int value ) { return value + number ; } return & add ; // Оператор & здесь необязателен, поскольку имя функции в C — это указатель, указывающий на себя }                int main ( void ) { fn_int_to_int add10 = adder ( 10 ); printf ( "%d \n " , add10 ( 1 )); return 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 ) { return value + number ; } return add ; } fn_int_to_int add10 = adder ( 10 ); printf ( "%d \n " , add10 ( 1 )); return 0 ; }                                 

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

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

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

class  CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // Выражение "new Runnable() { ... }" является анонимным классом, реализующим интерфейс 'Runnable'. new Thread ( new Runnable () { void run () { // Он может читать конечные локальные переменные: calculate ( uri ); // Он может получать доступ к закрытым полям включающего класса: result = result + 10 ; } } ). start (); } }                                   

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

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

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

class  CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // Код () -> { /* код */ } является замыканием. new Thread (() -> { calculate ( uri ); result = result + 10 ; }). start (); } }                           

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

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

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

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

public static void main ( String [] args ) { Функция < Строка , Целое число > длина = s -> s.length () ;             System.out.println ( length.apply ( " Hello , world!" ) ) ; // Выведет 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 -- ; } copy ] autorelease ]; }                 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 замыкания реализуются делегатами — указателем на функцию, связанным с указателем контекста (например, экземпляром класса или стековым фреймом в куче в случае замыканий).

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

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

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

Объекты функций (C++)

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

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

Встроенные агенты (Eiffel)

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

ok_button.click_event.subscribe ( agent ( x , y : INTEGER ) do map.country_at_coordinates ( x , y ) .display end )       

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

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

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

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

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

typedef void ( * TMyFunctionPointer )( void );    

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

typedef void ( __closure * TMyMethodPointer )();   

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

Примечания

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

Ссылки

  1. ^ Сассман и Стил. «Схема: Интерпретатор расширенного лямбда-исчисления». «... структура данных, содержащая лямбда-выражение, и среда, которая будет использоваться, когда это лямбда-выражение применяется к аргументам». (Wikisource)
  2. ^ Тернер, Дэвид А. (2012). "Немного истории функциональных языков программирования" (PDF) . Международный симпозиум по тенденциям в функциональном программировании . Конспект лекций по информатике. Том 7829. Springer. стр. 1–20 См. 12 §2, примечание 8 для заявления о M-выражениях. doi :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. ^ Moses, Joel (июнь 1970 г.). «Функция FUNCTION в LISP, или почему проблема FUNARG должна называться проблемой окружения». ACM SIGSAM Bulletin (15): 13–27. doi :10.1145/1093410.1093411. hdl :1721.1/5854. S2CID  17514262. AI Memo 199. Полезная метафора для различия между FUNCTION и QUOTE в LISP — думать о QUOTE как о пористом или открытом покрытии функции, поскольку свободные переменные ускользают в текущее окружение. FUNCTION действует как закрытое или непористое покрытие (отсюда термин «замыкание», используемый Ландином). Таким образом, мы говорим об «открытых» лямбда-выражениях (функции в LISP обычно являются лямбда-выражениями) и «закрытых» лямбда-выражениях. [...] Мой интерес к проблеме окружения начался, когда Ландин, который имел глубокое понимание проблемы, посетил MIT в 1966–67 годах. Затем я понял соответствие между списками FUNARG, которые являются результатами оценки «закрытых» лямбда-выражений в LISP , и лямбда-замыканиями ISWIM .
  5. ^ Викстрем, Оке (1987). Функциональное программирование с использованием стандартного ML . Прентис Холл. ISBN 0-13-331968-7. Причина, по которой это называется «замыканием», заключается в том, что выражение, содержащее свободные переменные, называется «открытым» выражением, и, связывая с ним привязки его свободных переменных, вы закрываете его.
  6. ^ Сассман, Джеральд Джей ; Стил, Гай Л. младший (декабрь 1975 г.). Схема: Интерпретатор для расширенного лямбда-исчисления (отчет). AI Memo 349.
  7. ^ Абельсон, Гарольд ; Сассман, Джеральд Джей ; Сассман, Джули (1996). Структура и интерпретация компьютерных программ. MIT Press. С. 98–99. ISBN 0-262-51087-1.
  8. ^ "array.filter". Mozilla Developer Center . 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. ^ "Function.prototype.bind()". MDN Web Docs . Получено 20 ноября 2018 г.
  14. ^ "Закрытия". MDN Web Docs . Получено 20 ноября 2018 г.
  15. ^ «Вложенные функции».
  16. ^ "Лямбда-выражения". Учебники Java .
  17. ^ "Вложенные, внутренние, членские и верхнеуровневые классы". Oracle Weblog Джозефа Д. Дарси . Июль 2007 г. Архивировано из оригинала 31 августа 2016 г.
  18. ^ "Пример внутреннего класса". Учебники Java: Изучение языка Java: Классы и объекты .
  19. ^ "Вложенные классы". Учебники Java: Изучение языка Java: Классы и объекты .
  20. ^ "Blocks Programming Topics". Apple Inc. 8 марта 2011 г. Получено 8 марта 2011 г.
  21. ^ Бенгтссон, Иоахим (7 июля 2010 г.). «Программирование с помощью блоков C на устройствах Apple». Архивировано из оригинала 25 октября 2010 г. Получено 18 сентября 2010 г.
  22. ^ Полную документацию можно найти по адресу http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure

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