Замыкания и [weak self]
В теме о замыканиях была карточка рецепта с запиской на скрепке: замыкание уносит с собой всё, что захватило. Теперь представьте библиотечную книгу, в кармашке которой лежит карточка читателя на эту же книгу. Пока карточка в кармашке, книга числится выданной. А чтобы достать карточку, книгу сначала нужно вернуть на полку.
Так бывает, когда объект хранит замыкание, а замыкание пользуется этим же объектом через self:
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Давайте разберём программу:
onFinish— свойство, в котором хранится замыкание без параметров и без результата, отсюда тип() -> Void. Сначала в нём пустое замыкание{}.- В
initвonFinishкладут новое замыкание. Оно пользуетсяself.nameи поэтому захватываетself, то есть сам объект, сильной ссылкой. player?.onFinish()вызывает замыкание, и программа печатаетAnna finished.player = nilубирает внешнюю ссылку, но строкиPlayer freedнет:deinitне сработал.
Получился цикл сильных ссылок. Объект держит замыкание через свойство onFinish, а замыкание держит объект через захваченный self. Второй участник цикла здесь не объект, а замыкание.
Здесь картинка с кармашком неточна: карточку в кармашке видно сразу, а захват спрятан в тексте замыкания. Чтобы его было видно, Swift требует писать self. явно. Без него строка print("\(name) finished") не скомпилируется:
error: reference to property 'name' in closure requires explicit use of 'self' to make capture semantics explicitСписок захвата
Цикл разрывают так же, как между двумя объектами: одну из ссылок делают weak. Свойство onFinish должно держать своё замыкание, поэтому weak делают ссылку замыкания на объект. Для этого есть список захвата — квадратные скобки перед словом in, где сказано, как захватить значения:
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Разберём, что изменилось:
[weak self]захватывает объект какweak-ссылку: замыкание знает объект, но не держит его.- Внутри замыкания
selfтеперь опционалPlayer?: когда замыкание вызовут, объекта может уже не быть. guard let self else { return }— краткая формаguard let self = self, такая же, какif let score. Если объект жив, нижеself— обычныйPlayer. Если нет, замыкание сразу выходит.player = nilубирает единственную сильную ссылку, и программа печатаетPlayer freed.
Если объекта уже нет
Замыкание может пережить свой объект, например если его сохранили в другой константе:
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], но обращаются к свойству, как раньше:
class Coach {
let name = "Boris"
var onStart: () -> Void = {}
init() {
onStart = { [weak self] in
print(self.name) // ✗
}
}
}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]программа аварийно завершится.