При копировании замыкания с сильным захватом объекта что удерживает объект после обнуления исходной ссылки?

При копировании замыкания с сильным захватом объекта что удерживает объект после обнуления исходной ссылки?

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

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

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

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

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

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

Для этого Swift формирует контекст замыкания. В нём находятся захваченные значения, включая сильные ссылки на экземпляры классов. ARC управляет временем жизни этого контекста так же, как и других ссылочных объектов.

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

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

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

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

При создании замыкания с сильным захватом в его контексте сохраняется сильная ссылка на текущее значение. Когда замыкание копируется, копируется значение замыкания или ссылка на его внутреннее хранилище; семантически каждая доступная копия сохраняет возможность использовать захваченный объект.

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

final class Session { deinit { print("deinit") } } var session: Session? = Session() var first: (() -> Void)? = { [session] in print(session != nil) } var second = first session = nil first = nil second = nil // теперь объект может быть уничтожен

В примере список захвата [session] сохраняет текущее значение session. После обнуления исходной переменной объект всё ещё удерживается замыканиями. Когда first и second обнулены, сильных ссылок на объект больше нет.

Это не означает, что Swift обязан выполнить отдельный retain для каждой копии или немедленно вызвать deinit в конкретной строке: оптимизатор может менять внутреннюю реализацию, сохраняя наблюдаемую семантику времени жизни. Надёжный вывод — объект не может быть уничтожен, пока существует сильный захват, необходимый живой копии замыкания.

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

Сервис создаётся при открытии экрана, а callback передаётся нескольким компонентам. Исходная переменная сервиса затем обнуляется при закрытии экрана, но один из компонентов продолжает хранить callback.

Варианты решения:

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

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

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

  1. Создаёт ли копирование замыкания отдельный экземпляр захваченного класса?

    Нет. Копируется значение замыкания и его контекст, но экземпляр класса остаётся тем же объектом. Две копии могут независимо вызывать методы одного экземпляра; изменение его свойств будет видно через обе копии.

  2. Может ли объект быть уничтожен, если одна копия замыкания всё ещё существует, но никогда не будет вызвана?

    Не при обычном сильном захвате. Для ARC важно наличие сильной ссылки, а не будущий факт вызова. Даже невызванное замыкание удерживает захваченный объект, пока само замыкание не освобождено или не заменено.

  3. Что изменится при копировании замыкания со слабым захватом?

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