Что такое замыкание
Замыкание, как правило, используется функциональными языками программирования, где они связывают функцию с определенным типом параметров, это позволяет дать доступ к переменным, находящимся за пределами границы функции. С использованием делегатов замыкание доступно в С#.
Что такое Замыкание?
Чаще всего, лексика замыкания используется в функциональных языках программирования. Замыкание – это специальный тип функции, с помощью которого она ссылается на свободные переменные. Это позволяет замкнутым функциям использовать переменные из внешнего окружения, несмотря на то что они не входят в границы. Когда функция создана, внешние переменные, которыми мы пользуемся, «захватываются», иными словами, они связаны с замкнутой функцией, так что они становятся доступными. Часто это обозначает то, что делаются копии значений переменных, когда инициализируется замыкание.
Использование замыкания в С#
В С# замыкание может быть создано с помощью анонимного метода или лямбда-выражения, все зависит от версии .NET framework, на которой вы разрабатываете. Когда вы создаете функцию, переменные, что используются в ней и находятся за областью видимости, скопированы и хранятся в коде с замыканием. Они могут использоваться везде, где вы вызовете оператор delegate. Это дает огромную гибкость при использовании делегатов, но также создает возможность неожиданных багов. К этому мы вернемся позже. А пока, давайте рассмотрим простой пример замыкания.
В коде, который ниже, мы создаем переменную «nonLocal» типа integer. Во второй строчке создаем экземпляр делегата «Action», что выводит в сообщение значение переменной типа integer. В конце мы запускаем функцию-делегат, чтобы увидеть сообщения.
int nonLocal = 1;
Action closure = delegate
Console.WriteLine( » + 1 = » , nonLocal, nonLocal + 1);
closure(); // 1 + 1 = 2
Мы можем сделать то же самое с лямбда-выражением. В следующем коде мы используем «lambda» для вывода информации, при этом лямбда-выражение имеет одинаковую силу.
int nonLocal = 1;
Console.WriteLine( » + 1 = » , nonLocal, nonLocal + 1);
closure(); // 1 + 1 = 2
Замыкания и переменные за пределами
С помощью анонимных методов или лямбда-выражения примеры выше,при этом получаем те результаты, что вы могли ожидать, так как захват переменных замыканием не очевиден сразу же. Мы можем сделать его более явным, изменяя пределы делегатов.
Рассмотрим следующий код. Здесь замыкание находится в классе «program» с переменной «action». В главном методе вызываем метод «SetUpClosure» для инициализации замыкания перед его использованием. Метод «SetUpClosure» очень важен. Вы можете увидеть, что переменная типа integer создана и инициализирована, и только тогда используется замыкание. В конце метода «SetUpClosure» эта переменная типа integer выходит за пределы. Однако, мы все еще вызываем делегат после этого. Скомпилируется и запустится ли этот код правильно? Произошло ли исключение при получении доступа к переменной за пределами? Попробуйте выполнить код.
static Action _closure;
static void Main( string [] args)
_closure(); // 1 + 1 = 2
private static void SetUpClosure()
int nonLocal = 1;
Console .WriteLine( » + 1 = » , nonLocal, nonLocal + 1);
Вы могли заметить, что мы получили одинаковый результат как и в оригинальном примере. Это и есть замыкание в действии. Переменная «nonLocal» была охвачена или «замкнута» кодом delegate, в результате чего она остается в нормальных пределах. По сути, переменная будет доступна, пока никаких дальнейших ссылок на делегат не останется.
Несмотря на то, что мы увидели замыкание в действии, они не поддерживаются С# и .NET framework. То, что действительно происходит — это работа на заднем фоне компилятора. Когда вы создаете собственные проекты, компилятор генерирует новые, скрытые классы, инкапсулируют нелокальную переменную и описанный код в анонимный метод или лямбда-выражение. Код, описанный в методе, и нелокальная переменная представлены в виде полей. Этот новый метод класса вызовется, когда делегат выполняется.
Автоматически сгенерированный класс для нашего простого замыкания — аналогичный приведенному ниже:
private sealed class c__DisplayClass1
public int nonLocal;
public void b__0()
Console.WriteLine( » + 1 = » , this .nonLocal, this .nonLocal + 1);
Замыкание захватывает переменную, а не его значение
В некоторых языках программирования определяют значение переменной, которая используется в замыкании. В С# захватываются сами переменные. Это важное отличие, так как мы можем изменять значение переменной за пределами функции. Для иллюстрации рассмотрим следующий код. Здесь мы создаем замыкание, которое выводит наше начальное математическое значение переменной. При создании делегатов значение переменной типа integer равно 1. Но после того замыкания, как мы объявили замыкание, и перед тем, как его вызвали, значение переменной поменялось на 10.
int nonLocal = 1;
Action closure = delegate
Console.WriteLine( » + 1 = » , nonLocal, nonLocal + 1);
Так как нелокальная переменная имела значение 1 перед созданием замыкания, вы могли бы ожидать, что результатом вывода будет «1+1=2». На самом деле, на других языках программирования так бы и было. Однако, так как мы изменили значение переменной до вызова функции замыкания, это значение влияет на выполнение функции замыкание. В действительности, вы увидите на дисплее:
Изменения в нелокальную переменную внутри функции замыкания также передаются в другом направлении. В следующем коде внутри делегата изменяем значение переменной перед тем, как объявленный код выведет ее. Изменения видны во внешней части кода несмотря на то, что происходят они внутри замыкания.
int nonLocal = 1;
Action closure = delegate
Переменная, которую мы изменяем, может привести нас к неожиданным багам в нашем коде. Мы можем продемонстрировать эту проблему в другом примере. На этот раз мы используем замыкание в простом алгоритме: многопоточное или параллельное программирование. Код ниже показывает цикл for, который имеет 5 новых потоков. Каждая пауза короткая, перед выводом значения переменной внутри цикла. Если значение переменной в цикле были захвачены, мы увидим цифры от 1 до 5 показаны в консоли, хотя, возможно, не в правильном порядке. Однако, так как эта переменная находится внутри замыкания и цикл закончится до того, как переменные будут выведены в сообщение, в конечном итоге мы увидим значение 6 для каждого потока.
for ( int i = 1; i
new Thread( delegate ()
К счастью, такая проблема легко устраняется, когда вы понимаете, что переменные, а не их значения захватываются. Все, что нам нужно сделать, это создать новую переменную для каждого прохождения(итерации) цикла. Это объявление можно записать в теле цикла и давать значение в управляющую переменную. При нормальных обстоятельствах временная переменная будет находится за переделами, когда цикл закончится, но замыкание будет связывать и поддерживать ее.
В коде ниже вы можете увидеть 5 примеров «значений», переменные, созданные и им назначенные 5 различных значений, каждая из них привязана к разному потоку.
for ( int i = 1; i
new Thread( delegate ()
Обратите внимание: вывод может меняться в зависимости от порядка, в котором потоки выполняются.
Лексическая область видимости это статическая область в JavaScript, имеющая прямое отношение к доступу к переменным, функциям и объектам, основываясь на их расположении в коде. Вот пример:
Тут функция inner имеет доступ к переменным в своей области видимости, в области видимости функции outer и глобальной области видимости. Функция outer имеет доступ к переменным, объявленным в собственной области видимости и глобальной области видимости.
В общем, цепочка области видимости выше будет такой:
Обратите внимание, что функция inner окружена лексической областью видимости функции outer , которая, в свою очередь, окружена глобальной областью видимости. Поэтому функция inner имеет доступ к переменным, определенным в функции outer и глобальной области видимости.
Замыкание — это возможность внутри функции обращаться к переменным, объявленным не в самой функции, а в одном из содержащих её блоков.
Ещё замыканием можно назвать всё множество таких переменных.
В твоём примере переменная name доступна через замыкание.
Замыкание – это функция вместе со всеми внешними переменными, которые ей доступны. То есть, замыкание – это функция + внешние переменные. Тем не менее, в JavaScript есть небольшая терминологическая особенность. Обычно, говоря «замыкание функции», подразумевают не саму эту функцию, а именно внешние переменные. Иногда говорят «переменная берётся из замыкания». Это означает – из внешнего объекта переменных. (источник: https://learn.javascript.ru/closures)
Способ, с помощью которого я навсегда запомнил замыкания — это сравнение их с рюкзаком. Когда функция создана и передаётся куда-либо, или возвращается из другой функции, то она носит с собой рюкзак. А в этом рюкзаке хранятся все переменные, которые были в области видимости во время создания этой функции. (Источник: https://medium.com/nuances-of-programming/я-никогда-не-понимал-замыкания-в-javascript-часть-первая-3c3f02041970 )
Замыкания на практике
Замыкания в JavaScript являются очень интересной вещью. Они позволяют связать некоторые данные с функцией. Это очень похоже на то, как это реализовано в объекте, который позволяет связать свойства (переменные) и методы (действия над этими переменными). Такие задачи в веб-разработке попадаются очень часто. Давайте рассмотрим одну из подобных задач.
Допустим, необходимо создать несколько модальных окон на странице с привязкой их к конкретным кнопкам. Кроме этого в задании говорится ещё о том, что необходимо сделать так, чтобы можно было легко менять при необходимости заголовок и содержимое модального окна.
Кнопки, открывающие модальные окна:
Функция, возвращая в качестве результата другую функцию:
Код, который выполняет создание модальных окон и установлением каждому из них заголовка и некоторого содержимого:
Итоговый код (кнопки + скрипт):
Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:
Защита от короткого замыкания
Большинство современных способов защиты от короткого замыкания основаны на принципе разрыва электрической цепи, при обнаружении КЗ.
Самые простые устройства, которые есть во многих электроприборах, защищающие от последствий коротких замыканий – это плавкие предохранители.
Чаще всего, плавкий предохранитель представляет собой проводник, рассчитанный на определенный предельный ток, который он сможет пропускать через себя, при превышении этого значения, проводник разрушается, тем самым разрывая электрическую цепь. Плавкий предохранитель – это самый слабый участок электрической цепи, который первый выходит из строя под действием высокого тока, тем самым защищает все остальные элементы.
Для защиты от коротких замыканий в квартире или доме, используются автоматические выключатели -АВ (чаще всего их называют просто автоматы), они устанавливаются на каждую группу электрической сети.
Каждый автоматический выключатель рассчитан на определенный рабочий ток, при превышении которого он разрывает цепь. Это происходит либо с помощью теплового расцепителя, который при нагреве, вследствие протекания высокого тока, механически разъединяет контакты, либо с помощью электромагнитного.
Принцип работы автоматических выключателей — это тема отдельной статьи, о них мы поговорим в другой раз. Сейчас же, хочу еще раз напомнить, что от короткого замыкания не спасает УЗО, его предназначение совсем в другом.
Для того, чтобы правильно выбрать защитный автоматический выключатель, делаются расчеты величины возможного тока короткого замыкания для конкретной электроустановки. Чтобы в случае, если КЗ произойдёт, автоматика сработала оперативно, не пропустив резко возросший ток и не сгорев от него, не успев разорвав цепь.
Когда js-программа только начинает свою работу, в ней еще нет ни переменных, ни функций, ни одного замыкания – ничего. Один лишь чистый глобальный контекст выполнения. На этом не паханом поле программист обладает полной свободой действий.
Чтобы отслеживать, куда забредет разработчик при отсутствии ограничений, существует стек выполнения. Это специальная структура, в которую складываются все активные контексты. Изначально он всего один, поэтому принципиальная схема выглядит так:
Итак, эти две структуры присутствуют всегда. Глобальный контекст исчезает только тогда, когда завершается сама программа.
В контексте можно объявлять и вызывать функции, инициализировать переменные, выполнять выражения. Все, что создано в рамках контекста, относится к нему. Например, объявленные в глобальной области переменные будут называться глобальными.
Все это звучит так, как будто программист сам может создавать контексты и добавлять их в стек. Так оно и есть, разработчики постоянно делают это, даже не задумываясь. Оказывается, новая область выполнения создается при каждом вызове функции.
Контексты функций
Когда интерпретатор JavaScript встречает вызов функции, происходит много интересного. Прежде всего, создается абсолютно новый контекст выполнения кода, который сразу же становится активным и перемещается на верх стека. Этот контекст называется локальным.
У любой области выполнения функции есть родитель – тот контекст, из которого она была вызвана, например, глобальный.
В локальной области еще до начала работы уже могут быть свои переменные – это входящие аргументы.
В качестве сигнала о завершении функции выступает закрывающая фигурная скобка > или ключевое слово return . Наткнувшись на них, движок готовится свернуть локальный контекст и вернуться к его родителю. Но прежде он должен получить возвращаемое значение.
Любая функция что-то возвращает, необязательно явно. Даже если в ней отсутствует команда return , родителю будет возвращено значение undefined . Что с ним делать – решит уже сам контекст вызова. Текущая локальная область будет уничтожена вместе со всеми своими переменными и функциями.
Чтобы досконально разобраться в концепции контекстов выполнения кода, нужно шаг за шагом пройти весь путь js-интерпретатора от начала до конца программы. Для примера подойдет очень простой код, в котором тем не менее происходит создание и вызов функции.
Все события кода разбираются детально, но с некоторыми упрощениями. Например, не учитывается хойстинг функций.
Итак, программа запущена, создан и помещен в стек глобальный контекст выполнения, переменных пока нет.
По следам локальных областей
1. Строка 1. Инструкция let a инициализирует первую глобальную переменную a , которая изначально равна undefined .
2. Присваивание переменной a значения 3.
3. Строка 2. Объявление функции addTwo в глобальном контексте.
4. Строки со 2 по 5 относятся к функции. В данный момент они просто пропускаются интерпретатором без анализа и выполнения.
5. Строка 6. Инициализация еще одной глобальной переменной b . На данный момент b = undefined .
6. Затем обработчик видит инструкцию присваивания. Чтобы выполнить ее, ему необходимо рассчитать правую часть выражения. Там находится вызов функции addTwo с входящим аргументом a .
7. Интерпретатор осматривает текущий глобальный контекст. Нужная функция находится на строке 2, а аргумент обнаруживается в самом начале программы ( a = 3 ). Все готово, можно запускать функцию.
8. Запуск сопровождается созданием нового контекста выполнения и переходом в него. Все дальнейшие действия осуществляются уже внутри локальной области addTwo . Не следует забывать, что эта функция в итоге должна сформировать некоторое значение и вернуть его в глобальный контекст.
8.1. Строка 2. Прежде всего, происходит инициализация параметра x и присвоение ему значения 3. Это первая локальная переменная области addTwo .
8.2. Строка 3. Создание в текущем контексте переменной result и занесение в нее числа 5 (результат сложения x + 2). Схема работы программы сейчас выглядит так:
8.3. Строка 4. Встретив завершающую команду, интерпретатор ищет переменную result . Она обнаруживается в текущем контексте на предыдущей строке. Таким образом, в глобальную область выполнения будет возвращено значение 5.
9. Сделав все, что полагается, обработчик с чистой совестью разрушает и удаляет из стека ненужный больше контекст функции addTwo вместе с переменными x и result .
10. Действие вновь возвращается в глобальную область на строку 6, где переменной b присваивается полученное из функции значение 5.
11. Строка 7. Здесь просто выводится на консоль для проверки результат работы программы.
Казалось бы, такая маленькая программа в 7 строк, а сколько событий!
Теперь понятно, что контекст выполнения – это просто актуальное на данный момент окружение кода. К нему относятся, например, входящие параметры и локальные переменные функции.
Область видимости
Прежде чем вызвать функцию или произвести какую-либо операцию с переменной, JavaScript должен их найти. Поиск начинается с текущего контекста выполнения. Если после тщательной проверки всех углов требуемое не найдено, интерпретатор не сдается. В этом случае он обратится к родительской области и поищет в ней.
При необходимости упорный обработчик дойдет по цепочке до самого глобального контекста. Здесь его полномочия заканчиваются. Если переменная не будет найдена в глобальной области, по месту требования вернется обидное значение undefined .
Из этого следует, что вложенные контексты выполнения имеют доступ к своим предкам любого уровня. Они могут смело пользоваться родительскими функциями и переменными, то есть видят их. Вот здесь и появляется понятие области видимости. Обратно это, кстати, не работает – дети от родителей свои игрушки прячут. Все как в жизни.
Замыкания активно эксплуатируют области видимости, поэтому следует уделить этой концепции особо пристальное внимание. Для полного осознания пройдем еще раз следом за интерпретатором по простой программе.
В программе есть глобальные переменные и переменные, которые будут созданы в локальном контексте multiplyThis . При этом функция во время работы попытается обратиться к переменной factor , которая лежит вне ее области выполнения. Получится ли у нее получить желаемое? Проследим за всеми перипетиями этой интриги.
Сказ о том, как JavaScript переменную искал
Начало не предвещает особенных потрясений: программа запускается, создает глобальный контекст выполнения и кладет его в стек. Затем в текущей области создается и получает значение переменная factor . Следом идет multiplyThis , содержащая определение функции, которое интерпретатор не трогает.
На шестой строчке события начинают, наконец, развиваться.
1. Новая переменная multiplied получает стартовое значение undefined .
2. Обработчик видит операцию присваивания и вызов multiplyThis с правой стороны от знака = . Он отправляется на поиски этой функции, находит ее в текущем глобальном контексте и вызывает с аргументом 6.
3. Создается контекст multiplyThis , в котором сразу же инициализируется входящий параметр n = 6 .
4. Строка 3. Создается еще одна локальная переменная result со значением undefined .
5. Чтобы осуществить присваивание, интерпретатору нужно найти значения n и factor. С первым проблем не возникает: оно лежит в текущем контексте выполнения и равно 6.
6. Но factor по-прежнему не найден. Без него ничего не выйдет, неужели все было напрасно?
Обработчик кода обращается к родительскому контексту с просьбой выдать переменную и получает то, что хотел. Теперь можно умножить два значения и записать результат.
7. Функция завершается инструкцией return , возвращаемое значение равно 12, локальная область multiplyThis разрушается вместе с переменными n и result . А вот factor остается, так как он лежит в родительском контексте.
Дальше все просто: в multiplied записывается то, что вернула функция, и переменная выводится на консоль.
Этот простой пример экспериментально подтвердил, что функция не ограничена лишь своим контекстом выполнения. Она может заглядывать к родителям и пользоваться их переменными. Более того, она даже может их менять, что не всегда хорошо. Таким образом, глобальный контекст всегда виден для всех прочих областей, созданных в программе.
Функция в функции
Замыкания уже очень-очень близко, осталось лишь рассмотреть функциональную матрешку JavaScript, на которой все и основано.
Функции из примеров выше возвращали простое число. Это не очень интересно. Почему бы не вернуть из функции другую функцию? Тем более, язык позволяет и даже одобряет подобные начинания.
Начало программы весьма прозаическое и отличается от предыдущих только именами глобальных переменных.
Сюжет закручивается на 9 строке при вызове функции createAdder .
1. Обработчик создает новую локальную область createAdder . Входящих параметров, требующих инициализации, у функции нет.
2. В текущем контексте объявляется переменная addNumbers .
3. Контекст завершает свою работу, возвращая описание функции.
Здесь следует сделать акцент на том, что возвращается только функциональное описание. Сама addNumbers будет разрушена вместе с локальной областью выполнения.
4. Теперь в adder записана функция, следовательно, ее можно вызвать. Что и делает программа на 10 строке.
5. Ожидаемо создается новая область adder , внутри которой инициализируются входящие аргументы a = 7 и b = 8 .
6. В новую локальную переменную result записывается результат их сложения, который и возвращается, когда функция отработает.
Замыкания — это такие функции, которые вы можете создавать в рантайме и им будет доступно текущее окружение, в рамках которого они были созданы. Другими словами, функции, определенные как замыкания, «запоминают» окружение, в котором они были созданы.
Перед тем как мы начнем разбираться с замыканиями, давайте определимся с функциями и анонимными функциями.
Анонимные функции
Функции, у которых есть имя — это именованные функции. Функции, которые могут быть созданы без указания имени — это анонимные функции. Все просто 🙂 Как можно видет в приведенном ниже коде, можно создать анонимную функцию и непосредственно вызвать или можно присвоить функцию некоторой переменной и вызвать с указанием этой переменной. В общем случае, для замыканий используются анонимные функции. В Go у вас есть возможность создать анонимную функцию и передать ее как параметр в другую функцию, таким образом мы используем функции высшего порядка.
Функция getPrintMessage создает анонимную функцию и возвращает ее. В printfunc сохраняется анонимная функция, которая затем вызывается.
Замыкания
Ниже, функция foo это внутренняя функция и у нее есть доступ к переменной text , определенной за рамками функции foo но внутри функции outer . Вот эта функция foo и называется замыканием. Она как бы замыкает переменные из внешней области видимости. Внутри нашего замыкания переменная text будет доступна.
Возвращаем замыкание и используем его снаружи
В этом примере покажем как можно возвращать замыкание из функции, в которой оно было определено. foo это замыкание, которое возвращается в главную функцию когда внешняя функции вызывается. А вызов самого замыкания происходит в момент, когда используются () . Этот код выводит сообщение «Modified hello». Таким образом, в замыкании foo все еще доступна переменная text , хотя мы уже вышли из внешней функции.
Замыкание и состояние
Замыкания сохраняют состояние. Это означает, что состояние переменных содержится в замыкании в момент декларации. Что это значит:
- Состояние(ссылки на переменные) такие же как и в момент создания замыкания. Все замыкания созданные вместе имеют общее состояние.
- Состояния будут разными если замыкания создавались по разному.
Давайте посмотрим на код ниже. Мы реализуем функцию, которая принимает начальное значение и возвращает два замыкания: counter(str) и incrementer(incr) . И в этом случае, состояние(переменная start ) будет одинаковым для обоих замыканий. После следующего вызова функции counter , мы получим еще два замыкания с уже новым состоянием.
В нашем примере при первом вызове counter(100) мы получаем замыкания ctr , intr в которых сохранен один и тот же указатель на 100.
Как видите, изначально оба значение равны 100. И когда мы увеличиваем значение с помощью incr() , замыкание ctr1() выводит старое значение, а ctr() выводит уже 101. Точно так же, если вызывать замыкание incr1() , то ctr() будет всегда выводить 101, а ctr1() будет показывать новые значения.
Ловушки
Одна из самых очевидных ловушек — это создание замыканий в цикле. Рассмотрим пример кода ниже.
Мы создаем 4 замыкания в цикле и возвращаем слайс с замыканиями. Каждое замыкание выполняет одинаковые действия: выводит индекс и значение по этому индексу. Главная функция проходит по слайсу и вызывает все эти замыкания.
Посмотрим на результат выполнения скрипта
Не очень приятный сюрприз. Давайте разберемся почему. Если мы вернемся на пару шагов назад и вспомним, что замыкания создаются единожды и имеют общее состояние, то проблема начинает проясняться. В нашем случае, все замыкания ссылаются на одни и теже переменные i и arr . Когда замыкания вызываются в главной функции, значение i равно 3 и значение во всех замыканиях мы получаем по ключу arr[3] .
Что в результате
Надеюсь, что поле прочтения этой статьи, вы чуть лучше понимаете принципы работы с замыканиями и теперь вам проще читать код в котором используются замыкания.
Для более углубленного понимания рекомендую прочитать две замечательные статьи, указанные ниже. В этих статьях говорится о замыканиях в контексте JavaScript, но для понимания основ это не так важно.