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

Вызывается ли deinit у экземпляра класса, если его инициализатор завершился ошибкой?

Вызывается ли deinit у экземпляра класса, если его инициализатор завершился ошибкой?

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

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

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

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

ARC автоматизировал подсчёт ссылок, но не изменил правило, что deinit относится только к успешно созданному экземпляру. Это предотвращает обращение к объекту, который не прошёл инициализацию и не удовлетворяет инвариантам своего типа.

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

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

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

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

При ошибке из init Swift не передаёт частично созданный экземпляр в обычный жизненный цикл ARC. Поэтому его deinit не запускается.

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

final class Resource { deinit { print("resource deinit") } } final class Session { let resource: Resource init() throws { resource = Resource() throw NSError(domain: "Init", code: 1) } deinit { print("session deinit") } }

В этом примере напечатается сообщение для Resource, но не для Session. Ресурс освобождается при очистке инициализированного свойства, а Session не считается успешно созданным экземпляром.

Ресурсы, требующие симметричного отката, лучше захватывать во временные локальные значения и освобождать непосредственно на ветке ошибки либо передавать владение отдельному объекту, который уже полностью инициализирован. deinit подходит для очистки обычных stored-свойств успешного экземпляра, но не как универсальный обработчик неудачного init.

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

Класс открывает временный файл в начале init, затем проверяет конфигурацию и обнаруживает ошибку. Вариант с записью закрытия файла только в deinit ненадёжен: при ошибке инициализации deinit этого класса не будет вызван.

Можно закрывать файл вручную на каждой ветке ошибки, но при добавлении новых этапов легко пропустить очистку. Более устойчивый вариант — создать отдельный полностью инициализированный владеющий объект-обёртку и хранить ресурс в нём; при выходе из области ошибки ARC освободит обёртку, а её deinit закроет файл.

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

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

  1. Вопрос: Вызывается ли deinit у свойства-объекта, если инициализатор его владельца завершился ошибкой?

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

  2. Вопрос: Может ли код после throw из инициализатора использовать частично созданный экземпляр?

    Ответ: Нет. Экземпляр не возвращается вызывающему коду как значение и не становится доступным для обычного использования. Вызвавшая сторона получает ошибку, а Swift очищает инициализированные части объекта.

  3. Вопрос: Можно ли рассчитывать на deinit для отката внешней операции, начатой внутри init?

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