Захват значений
Карточку с рецептом можно отдать вместе с запиской, прикреплённой скрепкой: «соли уже положили две ложки». Записка уходит вместе с карточкой, и её дописывают по ходу дела: положили ещё ложку — исправили число на записке.
Вы уже знаете, что замыкание видит константы и переменные вокруг себя. А что будет, если функция, в которой объявлена переменная, уже завершилась? Посмотрим на счётчик:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
let other = makeCounter()
print(other()) // 1
print(counter()) // 3Заголовок func makeCounter() -> () -> Int читают так: функция без параметров возвращает функцию без параметров, а та возвращает Int. У замыкания после return нет параметров, поэтому заголовок с in можно не писать: тип () -> Int компилятор берёт из заголовка makeCounter. Давайте разберём программу:
makeCounter()создаёт переменнуюcountсо значением0, возвращает замыкание и завершается. Замыкание попадает вcounter.- Каждый вызов
counter()увеличиваетcountна единицу и возвращает её: сначала1, потом2. - Второй вызов
makeCounter()создаёт новую переменнуюcount, и счётotherначинается с1. - Последний
counter()продолжает свой счёт и печатает3: вызовotherего не тронул.
Захват
Когда замыкание пользуется переменной из окружающего кода, говорят, что оно её захватывает. Захват устроен так:
- замыкание держит саму переменную, а не копию её значения, поэтому следующий вызов видит каждое изменение
count; - захваченная переменная живёт, пока живёт замыкание, хотя вызов
makeCounterдавно закончился; - каждый вызов
makeCounterсоздаёт новуюcount, поэтому уcounterиotherсчёт свой.
Обратите внимание: counter — константа let, а счёт всё равно растёт. let запрещает положить в counter другое замыкание, но не мешает замыканию менять захваченную count. Сама count объявлена через var, иначе её нельзя было бы менять.
Нажимайте «Следующий шаг» и следите за строкой «захвачена»: рамка makeCounter исчезает, а count остаётся у замыкания и растёт.
1func makeCounter() -> () -> Int {2 var count = 03 return {4 count += 15 return count6 }7}8 9let a = makeCounter()10print(a())11print(a())12let b = makeCounter()13print(b())
Программа вызывает makeCounter и ждёт, что та вернёт.
Пока жив счётчик, жива и его переменная. Снаружи до count не добраться: изменить счёт можно только вызовом самого счётчика.
Замыкание — ссылочный тип
Массив — значимый тип: после var second = first у вас два независимых массива. С замыканием всё иначе:
let same = counter
print(same()) // 4
print(counter()) // 5Разберём по строкам:
let same = counterне создаёт новый счётчик и не копируетcount. Теперь у одного замыкания два имени.same()продолжает тот же счёт: до этогоcounter()вернул3, поэтому теперь4.counter()видит это изменение и возвращает5.
Тип, у которого присваивание и передача в функцию не копируют значение, а дают ещё одно имя тому же самому, называют ссылочным типом. Замыкания в Swift — ссылочный тип.
Здесь картинка с запиской перестаёт работать. Карточку с запиской можно отксерокопировать, и у копии будет своя записка. А let same = counter копии не создаёт: оба имени ведут к одной карточке и одной записке.
Этот вопрос часто задают на собеседовании: замыкания — значимый или ссылочный тип? Ответ:
- ссылочный: после
let same = counterоба имени ведут к одному замыканию; - захваченные переменные у них общие, и изменение через одно имя видно через другое.
Частая ошибка
Переменную счёта объявили внутри замыкания:
func makeCounter() -> () -> Int {
return {
var count = 0 // ✗
count += 1
return count
}
}
let counter = makeCounter()
print(counter(), counter()) // 1 1Ошибки нет, но счёт не растёт. Теперь строка var count = 0 выполняется при каждом вызове замыкания, и каждый вызов начинает с нуля. Объявите count в теле makeCounter, перед return: тогда переменная одна на весь счётчик и замыкание её захватывает.
Главное
- Замыкание захватывает переменные, которыми пользуется: держит саму переменную, а не копию значения.
- Захваченная переменная живёт, пока живёт замыкание, даже после конца вызова, где её объявили.
- Каждый вызов
makeCounterсоздаёт новую переменную и новый счётчик. - Замыкание — ссылочный тип: после
let same = counterоба имени делят одно замыкание и его переменные.