При создании callback объект уже существует, но позже переменная service переназначается. К какому объекту обратится callback?
final class Service {
let id: String
init(_ id: String) { self.id = id }
deinit { print("deinit \(id)") }
}
var service: Service? = Service("A")
let callback: () -> Void = { [weak service] in
print(service?.id ?? "nil")
}
service = Service("B")
callback()
callback обратится к объекту Service("A"), захваченному в момент создания замыкания. Поскольку захват слабый, callback не удерживает этот объект; после присваивания service = Service("B") экземпляр A обычно освобождается, поэтому callback напечатает nil, а не B.
Замыкания часто живут дольше места, где они были созданы, и могут случайно продлевать жизнь объектов. В Swift списки захвата позволяют явно задать, какие значения и с каким типом владения должны попасть в замыкание: сильное, слабое или невладеющее.
Это решает две разные задачи: управление временем жизни захваченных объектов и фиксацию конкретного значения на момент создания замыкания. Эти свойства особенно важны для callback-ов, хранящихся в объектах или передаваемых асинхронным операциям.
Список захвата [weak service] не означает «следи за переменной service». Он сохраняет текущий объект как слабую ссылку. Последующее присваивание другой ссылки в переменную не изменяет захваченное значение.
Если ожидать, что callback автоматически начнёт использовать Service("B"), он будет обращаться к неверному объекту. Если же захватить сервис сильно, callback может неожиданно продлить жизнь устаревшего экземпляра.
При создании замыкания выражение service вычисляется сразу. Слабая ссылка внутри списка захвата начинает указывать на текущий экземпляр A, но не увеличивает его strong reference count.
После выполнения service = Service("B") переменная начинает сильно владеть B. Сильное владение A прекращается, других сильных ссылок на A нет, поэтому ARC освобождает A и обнуляет слабую ссылку внутри callback. Вызов callback безопасен и печатает nil.
Объект B не попадает в callback: он был присвоен переменной уже после формирования списка захвата. Чтобы callback использовал актуальный сервис, нужно либо создавать его заново после замены объекта, либо захватывать контейнер состояния, содержащий изменяемую ссылку.
Сильный захват [service] сохранил бы A и позволил callback напечатать A, но продлил бы его жизнь. Слабый захват предотвращает такое удержание, однако callback должен корректно обрабатывать отсутствие объекта.
В отличие от [weak service], обычный захват локальной переменной без списка захвата может сохранять общий контекст переменной. Тогда замыкание способно увидеть последующее присваивание этой переменной. Это другой механизм и его нельзя подменять поведением списка захвата.
Представим, что callback создаётся для конкретного экземпляра сетевого клиента, а затем приложение меняет клиент после обновления конфигурации. Вариант с сильным захватом гарантирует доступность старого клиента, но удерживает устаревший объект и связанные с ним ресурсы. Вариант с [weak client] не создаёт такого удержания, но после освобождения клиента callback ничего не выполнит.
Оптимальное решение зависит от смысла операции. Если callback относится именно к старому клиенту, его следует явно создать заново при смене клиента или передать нужный экземпляр параметром. Если допустимо пропустить операцию при отсутствии клиента, слабый захват с проверкой nil безопаснее и не создаёт лишнего владения.
Практический результат — callback либо работает с чётко зафиксированным экземпляром, либо явно отражает отсутствие объекта. Не следует рассчитывать, что [weak variable] будет динамически отслеживать переназначение переменной.
Что изменится, если убрать weak из списка захвата?
callback будет сильно владеть объектом A. После присваивания service = Service("B") объект A не освободится, пока существует callback. Вызов напечатает A. Это может быть правильным решением для гарантированного завершения операции, но опасным, если callback хранится долго.
Можно ли сделать callback динамически использующим текущий сервис?
Да, нужно захватывать не снимок объекта, а изменяемое состояние, например ссылочный контейнер с полем service, либо передавать сервис в callback параметром. Тогда callback читает актуальное значение в момент вызова, а не значение, зафиксированное списком [weak service].
Почему слабая ссылка внутри callback обнуляется автоматически?
weak-ссылка не владеет объектом и поддерживается ARC как zeroing weak reference. Когда объект уничтожается, рантайм делает такие ссылки nil, поэтому последующий доступ через optional безопасен. Это отличается от unowned: невладеющая ссылка не становится optional и обращение к уже уничтоженному объекту приводит к ошибке времени выполнения.