Программирование SwiftARC и памятьРазработчик мобильных приложений на Swift

В цикле ссылок между объектом владельцем и делегатом какую ссылку выбрать, если делегат может исчезнуть ран...

В цикле ссылок между объектом-владельцем и делегатом какую ссылку выбрать, если делегат может исчезнуть раньше владельца?

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

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

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

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

До ARC разработчик вручную управлял временем жизни объектов: увеличивал и уменьшал счётчик ссылок. Ошибки в этом управлении приводили к утечкам памяти, преждевременному освобождению и сбоям.

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

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

Если владелец сильно удерживает делегата, а делегат сильно удерживает владельца, их счётчики ссылок не обнуляются. Оба объекта остаются в памяти даже после того, как приложение больше не может к ним обратиться.

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

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

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

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

final class Owner { weak var delegate: Delegate? } final class Delegate {} let owner = Owner() var delegate: Delegate? = Delegate() owner.delegate = delegate delegate = nil print(owner.delegate == nil) // true

Сильная ссылка delegate в примере удаляется, после чего объект делегата освобождается, а ссылка владельца автоматически обнуляется. Для делегата, который может исчезнуть раньше владельца, это предпочтительнее потенциального сбоя через unowned.

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

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

Контроллер владеет сервисом загрузки, а сервис хранит замыкание обратного вызова. Если замыкание захватывает контроллер сильно, возникает цикл: контроллер удерживает сервис, сервис удерживает замыкание, а замыкание удерживает контроллер.

Рассматривались три варианта:

  • Сильный захват контроллера — прост в использовании, но может удерживать экран после закрытия.
  • unowned — не создаёт цикла и не требует опционального значения, но приводит к сбою, если сервис завершит работу после уничтожения контроллера.
  • weak — не создаёт цикла и безопасно пропускает callback для уже уничтоженного контроллера, но требует проверки optional-ссылки.

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

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

  1. Почему weak автоматически становится nil, а unowned вызывает сбой?

    weak поддерживается специальным механизмом отслеживания слабых ссылок. При освобождении объекта Swift обновляет связанные слабые ссылки, поэтому последующее чтение возвращает nil.

    unowned хранит ссылку без владения и без безопасного обнуления для вызывающего кода. Swift доверяет заявленной гарантии времени жизни; если она нарушена, проверка при обращении обнаруживает недействительную ссылку и вызывает ошибку времени выполнения.

  2. Может ли weak сама по себе предотвратить любой цикл ссылок?

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

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

  3. Когда unowned предпочтительнее weak?

    unowned подходит, когда жизненный цикл одного объекта строго включает жизненный цикл другого и это гарантировано архитектурой. Классический случай — объект, который не может существовать без своего владельца, но при этом не должен владеть им обратно.

    Если гарантия зависит от порядка отмены подписки, асинхронной операции или внешнего компонента, безопаснее использовать weak. Небольшое неудобство optional-проверки обычно предпочтительнее аварийного завершения приложения.