Перейти к содержимому
Теория · Замыкания · урок 7 из 8

Захват значений

Карточку с рецептом можно отдать вместе с запиской, прикреплённой скрепкой: «соли уже положили две ложки». Записка уходит вместе с карточкой, и её дописывают по ходу дела: положили ещё ложку — исправили число на записке.

Вы уже знаете, что замыкание видит константы и переменные вокруг себя. А что будет, если функция, в которой объявлена переменная, уже завершилась? Посмотрим на счётчик:

SWIFT
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. Давайте разберём программу:

  1. makeCounter() создаёт переменную count со значением 0, возвращает замыкание и завершается. Замыкание попадает в counter.
  2. Каждый вызов counter() увеличивает count на единицу и возвращает её: сначала 1, потом 2.
  3. Второй вызов makeCounter() создаёт новую переменную count, и счёт other начинается с 1.
  4. Последний counter() продолжает свой счёт и печатает 3: вызов other его не тронул.

Захват

Когда замыкание пользуется переменной из окружающего кода, говорят, что оно её захватывает. Захват устроен так:

  • замыкание держит саму переменную, а не копию её значения, поэтому следующий вызов видит каждое изменение count;
  • захваченная переменная живёт, пока живёт замыкание, хотя вызов makeCounter давно закончился;
  • каждый вызов makeCounter создаёт новую count, поэтому у counter и other счёт свой.

Обратите внимание: counter — константа let, а счёт всё равно растёт. let запрещает положить в counter другое замыкание, но не мешает замыканию менять захваченную count. Сама count объявлена через var, иначе её нельзя было бы менять.

Нажимайте «Следующий шаг» и следите за строкой «захвачена»: рамка makeCounter исчезает, а count остаётся у замыкания и растёт.

ВживуюДва счётчикашаг 1 из 12 · рамок: 1

Программа вызывает makeCounter и ждёт, что та вернёт.

Строка «захвачена» — переменные, захваченные замыканиями

Пока жив счётчик, жива и его переменная. Снаружи до count не добраться: изменить счёт можно только вызовом самого счётчика.

Замыкание — ссылочный тип

Массив — значимый тип: после var second = first у вас два независимых массива. С замыканием всё иначе:

SWIFT
let same = counter
print(same()) // 4
print(counter()) // 5

Разберём по строкам:

  1. let same = counter не создаёт новый счётчик и не копирует count. Теперь у одного замыкания два имени.
  2. same() продолжает тот же счёт: до этого counter() вернул 3, поэтому теперь 4.
  3. counter() видит это изменение и возвращает 5.

Тип, у которого присваивание и передача в функцию не копируют значение, а дают ещё одно имя тому же самому, называют ссылочным типом. Замыкания в Swift — ссылочный тип.

Здесь картинка с запиской перестаёт работать. Карточку с запиской можно отксерокопировать, и у копии будет своя записка. А let same = counter копии не создаёт: оба имени ведут к одной карточке и одной записке.

Этот вопрос часто задают на собеседовании: замыкания — значимый или ссылочный тип? Ответ:

  • ссылочный: после let same = counter оба имени ведут к одному замыканию;
  • захваченные переменные у них общие, и изменение через одно имя видно через другое.

Частая ошибка

Переменную счёта объявили внутри замыкания:

SWIFT
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 оба имени делят одно замыкание и его переменные.