Перейти к содержимому
Теория · Память и ARC · урок 6 из 7

Замыкания и [weak self]

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

Так бывает, когда объект хранит замыкание, а замыкание пользуется этим же объектом через self:

SWIFT
class Player {
    let name = "Anna"
    var onFinish: () -> Void = {}
    init() {
        onFinish = { print("\(self.name) finished") } // ✗
    }
    deinit { print("Player freed") }
}

var player: Player? = Player()
player?.onFinish() // Anna finished
player = nil

Давайте разберём программу:

  1. onFinish — свойство, в котором хранится замыкание без параметров и без результата, отсюда тип () -> Void. Сначала в нём пустое замыкание {}.
  2. В init в onFinish кладут новое замыкание. Оно пользуется self.name и поэтому захватывает self, то есть сам объект, сильной ссылкой.
  3. player?.onFinish() вызывает замыкание, и программа печатает Anna finished.
  4. player = nil убирает внешнюю ссылку, но строки Player freed нет: deinit не сработал.

Получился цикл сильных ссылок. Объект держит замыкание через свойство onFinish, а замыкание держит объект через захваченный self. Второй участник цикла здесь не объект, а замыкание.

Здесь картинка с кармашком неточна: карточку в кармашке видно сразу, а захват спрятан в тексте замыкания. Чтобы его было видно, Swift требует писать self. явно. Без него строка print("\(name) finished") не скомпилируется:

TEXT
error: reference to property 'name' in closure requires explicit use of 'self' to make capture semantics explicit

Список захвата

Цикл разрывают так же, как между двумя объектами: одну из ссылок делают weak. Свойство onFinish должно держать своё замыкание, поэтому weak делают ссылку замыкания на объект. Для этого есть список захвата — квадратные скобки перед словом in, где сказано, как захватить значения:

SWIFT
class Player {
    let name = "Anna"
    var onFinish: () -> Void = {}
    init() {
        onFinish = { [weak self] in
            guard let self else { return }
            print("\(self.name) finished")
        }
    }
    deinit { print("Player freed") }
}

var player: Player? = Player()
player?.onFinish() // Anna finished
player = nil // Player freed

Разберём, что изменилось:

  1. [weak self] захватывает объект как weak-ссылку: замыкание знает объект, но не держит его.
  2. Внутри замыкания self теперь опционал Player?: когда замыкание вызовут, объекта может уже не быть.
  3. guard let self else { return } — краткая форма guard let self = self, такая же, как if let score. Если объект жив, ниже self — обычный Player. Если нет, замыкание сразу выходит.
  4. player = nil убирает единственную сильную ссылку, и программа печатает Player freed.

Если объекта уже нет

Замыкание может пережить свой объект, например если его сохранили в другой константе:

SWIFT
var guest: Player? = Player()
let finish = guest!.onFinish
guest = nil // Player freed
finish()

Константа finish держит само замыкание, но не объект. Поэтому guest = nil освобождает объект, и к вызову finish() в замыкании self уже равен nil. guard выходит, и программа ничего не печатает.

Другие записи

Через self?.

Вместо guard let self к объекту можно обращаться через ?., как к любому опционалу: print(self?.name ?? "nobody"). Если объекта уже нет, такое замыкание напечатает nobody.

[unowned self]

[unowned self] тоже не держит объект, и self внутри замыкания не опционал. Но если вызвать такое замыкание после освобождения объекта, программа аварийно завершится, как с любой unowned-ссылкой. Поэтому чаще пишут [weak self].

Этот вопрос часто задают на собеседовании: зачем нужен [weak self]? Ответ можно строить так:

  • объект, который хранит замыкание, держит его сильной ссылкой;
  • замыкание, которое пользуется self, тоже держит объект, и получается цикл сильных ссылок;
  • [weak self] захватывает объект как weak-ссылку, поэтому цикла нет;
  • внутри замыкания self становится опционалом, и его проверяют через guard let self.

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

Написали [weak self], но обращаются к свойству, как раньше:

SWIFT
class Coach {
    let name = "Boris"
    var onStart: () -> Void = {}
    init() {
        onStart = { [weak self] in
            print(self.name) // ✗
        }
    }
}
TEXT
error: value of optional type 'Coach?' must be unwrapped to refer to member 'name' of wrapped base type 'Coach'

Программа не компилируется: после [weak self] внутри замыкания self — опционал Coach?, и читать его свойства напрямую нельзя. Добавьте guard let self else { return } в начало замыкания или напишите print(self?.name ?? "nobody").

Главное

  • Объект, который хранит замыкание, и замыкание, которое пользуется self, держат друг друга: это цикл сильных ссылок.
  • Swift требует писать self. в таком замыкании явно, чтобы захват было видно.
  • Запись [weak self] в списке захвата даёт замыканию weak-ссылку на объект; внутри self — опционал, его проверяют через guard let self.
  • Если объекта уже нет, self в замыкании равен nil, и вызов безопасен, а с [unowned self] программа аварийно завершится.