Если свойство объекта хранит замыкание с захваченным unowned self, образуется ли retain cycle?

Если свойство объекта хранит замыкание с захваченным unowned self, образуется ли retain cycle?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет, retain cycle не образуется: захват unowned self не увеличивает счётчик сильных ссылок на объект. Объект владеет замыканием, но замыкание не владеет объектом.

Однако такая схема безопасна только пока объект гарантированно жив. Если замыкание будет вызвано после уничтожения объекта, обращение к unowned self завершится аварийной ошибкой времени выполнения.

Исторический контекст

ARC автоматизировал ручное управление подсчётом ссылок, но не может самостоятельно определить, какие связи в графе объектов должны быть владеющими. Поэтому Swift предоставляет явно различимые типы связей: сильные, weak и unowned.

unowned предназначен для отношений, где ссылка не должна продлевать жизнь объекта, но корректность программы предполагает, что объект всё ещё существует в момент обращения. Это позволяет разорвать цикл без поведения nil, характерного для weak.

Постановка проблемы

Типичная ситуация — объект хранит callback в собственном свойстве и хочет, чтобы callback обращался к этому же объекту. Сильный захват self создаёт цикл: объект удерживает замыкание, а замыкание удерживает объект.

Замена захвата на unowned self устраняет цикл, но переносит ответственность на разработчика. Если callback сохранён где-то ещё и пережил объект, его вызов обращается к недействительной ссылке и приводит к runtime trap.

Подробное решение

При захвате unowned self замыкание хранит некопирующую и невладеющую ссылку на экземпляр. Поэтому граф владения выглядит так: объект сильно владеет замыканием, а обратная связь от замыкания к объекту не является сильной.

Минимальный пример:

final class Screen { var action: (() -> Void)? = nil init() { action = { [unowned self] in self.render() } } func render() {} } var callback: (() -> Void)? do { let screen = Screen() callback = screen.action } callback?() // runtime trap

После выхода из блока screen уничтожается, потому что замыкание не удерживает его. Но само замыкание остаётся в callback. Поэтому последующий вызов пытается разыменовать уже уничтоженный объект.

Если время жизни объекта не гарантировано, следует использовать [weak self] и корректно обработать nil. Если объект обязан жить дольше замыкания по архитектурному инварианту, unowned точнее выражает намерение и не требует optional-проверки.

Компромисс такой: unowned не создаёт цикл и не требует ветвления по nil, но нарушение гарантии времени жизни превращается в аварийное завершение. weak безопаснее при неопределённом времени жизни, однако callback может ничего не сделать после уничтожения владельца.

Ситуация из практики

Экран хранит обработчик нажатия в свойстве onTap, а обработчик передаётся во внешний объект аналитики. Вариант с сильным захватом self может удерживать экран через цепочку экран → обработчик → экран, особенно если внешний объект также хранит callback.

Вариант с unowned self разрывает цикл и не допускает вызов после ожидаемого жизненного цикла экрана только при строгой гарантии, что внешний объект очищает callback до уничтожения экрана. Его плюс — отсутствие optional-проверок; минус — ошибка очистки приводит к аварийному завершению.

Вариант с weak self не создаёт цикл и корректно переживает уничтожение экрана: callback увидит nil и завершится без действия. Для UI-кода с независимым владельцем callback обычно выбирают именно weak, поскольку порядок очистки внешних подписчиков не всегда гарантирован. unowned оправдан, когда связь жизненного цикла доказана и нарушение инварианта является программной ошибкой.

Что кандидаты часто упускают

  1. Увеличивает ли unowned-захват счётчик сильных ссылок?

    Нет. unowned не продлевает время жизни объекта и не участвует в его удержании. В отличие от сильного захвата, он не добавляет владельца в граф ARC.

  2. Почему хранение замыкания в самом объекте не создаёт цикл при unowned self?

    Потому что цикл определяется только владеющими связями. Объект сильно владеет замыканием, но обратная ссылка замыкания на объект невладеющая. В графе присутствует однонаправленное владение, поэтому ARC может уничтожить объект, когда исчезнут остальные сильные ссылки.

  3. Что произойдёт, если объект уничтожен до вызова замыкания?

    Для unowned обращение к уничтоженному объекту не превращается в nil. Swift обнаруживает недействительное разыменование и аварийно завершает выполнение. Если такой порядок событий допустим, захват должен быть weak, а код callback — явно обрабатывать отсутствие объекта.