Подтвердить что ты не робот

Каково точное определение замыкания?

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

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

int count()
{
    static int x = 0;

    return x++;
}

Но все, что я читаю, похоже, подразумевает, что закрытие должно каким-то образом включать функции передачи в качестве объектов первого класса. Кроме того, обычно подразумевается, что закрытие не является частью процедурного программирования. Является ли это случаем решения, слишком связанного с проблемой, которую он решает, или я не понимаю точное определение?

4b9b3361

Ответ 1

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

От Wikipedia

Закрытие закрывается над свободными переменными (переменными, которые не являются локальными переменными)

Ответ 2

Нет, это не закрытие. Ваш пример - это просто функция, которая возвращает результат приращения статической переменной.

Вот как будет работать замыкание:

function makeCounter( int x )
{
  return int counter() {
    return x++;
  }
}

c = makeCounter( 3 );
printf( "%d" c() ); => 4
printf( "%d" c() ); => 5
d = makeCounter( 0 );
printf( "%d" d() ); => 1
printf( "%d" c() ); => 6

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

Изменить: Я думаю, что подобные примеры упрощают понимание, чем определения, но если вы хотите, чтобы определение, которое я бы сказал, "Закрытие представляет собой комбинацию функции и среды. Среда содержит переменные, которые определены в функции, а также функции, которые видны функции при ее создании. Эти переменные должны оставаться доступными для функции до тех пор, пока существует функция.

Ответ 3

Для точное определение, я предлагаю посмотреть его запись в Википедии. Это особенно хорошо. Я просто хочу пояснить это на примере.

Предположим, что этот фрагмент кода С# (который должен выполнять поиск AND в списке):

List<string> list = new List<string> { "hello world", "goodbye world" };
IEnumerable<string> filteredList = list;
var keywords = new [] { "hello", "world" };
foreach (var keyword in keywords)
    filteredList = filteredList.Where(item => item.Contains(keyword));

foreach (var s in filteredList)  // closure is called here
    Console.WriteLine(s);

Это общая ошибка в С#, чтобы сделать что-то подобное. Если вы посмотрите на выражение лямбда внутри Where, вы увидите, что он определяет функцию, которая зависит от значения переменной на своем сайте определения. Это как передача самой переменной функции, а не значение этой переменной. Фактически, когда это замыкание называется, оно извлекает значение переменной keyword в это время. Результат этого примера очень интересен. Он печатает и "мир привет" и "прощай мир", чего мы не хотели. Что случилось? Как я сказал выше, функция, которую мы объявили с помощью выражения лямбда, замыкание над переменной keyword, так вот что происходит:

filteredList = filteredList.Where(item => item.Contains(keyword))
                           .Where(item => item.Contains(keyword)); 

и во время выполнения закрытия keyword имеет значение "мир", поэтому мы в основном фильтруем список пару раз с тем же ключевым словом. Решение:

foreach (var keyword in keywords) {
    var temporaryVariable = keyword;
    filteredList = filteredList.Where(item => item.Contains(temporaryVariable));
}

Так как temporaryVariable привязан к телу цикла foreach, на каждой итерации это другая переменная. По сути, каждое закрытие связывается с отдельной переменной (это разные экземпляры temporaryVariable на каждой итерации). На этот раз он даст правильные результаты ( "hello world" ):

filteredList = filteredList.Where(item => item.Contains(temporaryVariable_1))
                           .Where(item => item.Contains(temporaryVariable_2));

в котором temporaryVariable_1 имеет значение "hello", а temporaryVariable_2 имеет значение "мир" во время выполнения закрытия.

Обратите внимание, что замыкания вызвали расширение времени жизни переменных (их жизнь должна была закончиться после каждой итерации цикла). Это также является важным побочным эффектом закрытия.

Ответ 4

Закрытие - это метод реализации для представления процедур/функций с локальным состоянием. Один из способов реализации замыканий описан в SICP. Во всяком случае, я покажу его суть.

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

// This is some C like language that has first class functions and closures.
function accumulator(counter) {
    return (function() { return ++counter; });
}

Что произойдет, когда мы оценим следующую строку?

accum1 = accumulator(0);

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

Я описал несколько практических применений закрытий в своем блоге http://vijaymathew.wordpress.com. (См. Сообщения "Опасные проекты" и "О передаче сообщений" ).

Ответ 5

Там уже много ответов, но я добавлю еще одного...

Закрытия не уникальны для функциональных языков. Например, они встречаются в Паскале (и семье), который имеет вложенные процедуры. Стандарт C их не имеет (пока), но IIRC есть расширение GCC.

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

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

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

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

Python лямбда - простой пример функционального стиля...

def parent () :
  a = "hello"
  return (lamda : a)

funcref = parent ()
print funcref ()

Мои питоны немного ржавые, но я думаю, это правильно. Дело в том, что вложенная функция (лямбда) по-прежнему относится к значению локальной переменной a, даже если parent вышла, когда она вызывается. Функция нуждается где-то, чтобы сохранить это значение, пока оно не понадобится, и это место называется закрытием.

Закрытие немного похоже на неявный набор параметров.

Ответ 6

Отличный вопрос! Учитывая, что одним из принципов ООП ООП является то, что объекты имеют поведение, а также данные, замыкания являются особым типом объекта, потому что их наиболее важной целью является их поведение. Тем не менее, что я имею в виду, когда говорю о своем "поведении"?

(Многое из этого рисуется из "Groovy в действии" Дирка Кенига, который является удивительной книгой)

На простейшем уровне закрытие - это действительно какой-то код, который завершен, чтобы стать андрогинным объектом/методом. Это метод, потому что он может принимать параметры и возвращать значение, но это также объект, в котором вы можете передать ссылку на него.

По словам Дирка, представьте себе конверт, внутри которого есть кусок бумаги. Типичный объект имел бы переменные и их значения, написанные на этом документе, но у закрытия вместо этого был бы список инструкций. Скажем, в письме говорится: "Передайте этот конверт и письмо своим друзьям".

In Groovy: Closure envelope = { person -> new Letter(person).send() }
addressBookOfFriends.each (envelope)

Объект замыкания здесь - это значение переменной огибающей, и она использует это параметр для каждого метода.

Некоторые детали: Сфера охвата. Объем закрытия - это данные и элементы, к которым можно получить доступ в нем. Возврат из закрытия: Закрытие часто использует механизм обратного вызова для выполнения и возврата из себя. Аргументы. Если для закрытия требуется всего 1 параметр, Groovy и другие langs предоставляют имя по умолчанию: "it", чтобы сделать кодирование быстрее. Так, например, в нашем предыдущем примере:

addressBookOfFriends.each (envelope) 
is the same as:
addressBookOfFriends.each { new Letter(it).send() }

Надеюсь, это то, что вы ищете!

Ответ 7

Объект - это функция состояния плюс. Закрытие - это функция плюс состояние.

Функция f является замыканием, когда она закрывается (захвачена) x

Ответ 8

Я думаю, что у Питера Эдди это правильно, но пример можно сделать более интересным. Вы можете определить две функции, которые перекрывают локальную переменную, увеличивают и уменьшают. Счетчик будет разделен между этой парой функций и уникален для них. Если вы определяете новую пару функций increment/decment, они будут делиться другим счетчиком.

Кроме того, вам не нужно передавать начальное значение x, вы можете позволить ему по умолчанию использовать нуль внутри функционального блока. Это сделает более ясным, что он использует значение, которое у вас больше нет обычного доступа.