При выбрасывающем инициализаторе класса, завершившемся ошибкой, может ли вызывающий код получить частично созданный экземпляр?
Нет. Если выбрасывающий инициализатор завершается ошибкой, экземпляр не возвращается вызывающему коду: доступен только выброшенный объект ошибки. Частично инициализированный объект нельзя использовать, а его обычный deinit не вызывается.
Инициализация должна сформировать объект, пригодный для безопасного использования. Поэтому Swift разделяет два исхода: инициализатор либо возвращает полностью созданный экземпляр, либо сообщает о невозможности создания через ошибку.
Такой подход позволяет сохранить причину сбоя, в отличие от init?, который сообщает только факт неудачи через nil. Для операций, где важно объяснить причину отказа, throws подходит лучше.
Ошибка может произойти после выделения ресурсов, но до завершения инициализации всех свойств. Например, объект может успеть открыть файл или зарегистрировать временный идентификатор, а затем получить ошибку конфигурации.
Нельзя рассчитывать на deinit как на универсальную очистку: при неудачной инициализации объект не считается полностью созданным. Если промежуточные ресурсы требуют освобождения, их нужно очищать явно, обычно с помощью defer внутри инициализатора.
При вызове выбрасывающего инициализатора выражение либо возвращает полностью инициализированный экземпляр, либо передаёт ошибку в ближайший catch или выше по стеку. Ссылки на self и доступ к его методам ограничены правилами инициализации: до завершения инициализации нельзя обращаться с объектом как с готовым.
В этом примере session получает nil, defer выполняется, а deinit не печатает ничего. defer подходит для очистки ресурсов, приобретённых до точки ошибки, потому что он выполняется при выходе из области даже из-за throw.
Failable initializer (init?) возвращает nil, когда причина отказа не нужна клиенту. Throwing initializer (init() throws) сохраняет диагностическую информацию и позволяет вызывающему коду различать причины ошибки. Альтернатива — статический фабричный метод, возвращающий Result; он удобен, когда результат нужно передавать как значение, а не распространять через throws.
Важный компромисс: чем больше ресурсов создаётся до потенциальной ошибки, тем сложнее обеспечить корректную очистку. Поэтому инициализатор желательно делать коротким, а внешние побочные эффекты выполнять после успешного создания объекта либо защищать их локальными defer.
Сервису нужен объект подключения, но конфигурация может быть повреждена, а секрет — недоступен. Вариант с init? прост, однако клиент получает только nil и не может отличить ошибку конфигурации от временной недоступности сервиса.
Можно использовать фабрику с Result: это удобно для асинхронных или конвейерных API, но объект не создаётся обычным синтаксисом и клиенту приходится отдельно разбирать Result. Можно также вернуть объект с невалидным внутренним состоянием, но такой дизайн переносит ошибку дальше и усложняет инварианты класса.
Предпочтительный вариант — init() throws с типизированной ошибкой, например ConfigurationError. Инициализатор создаёт только валидные экземпляры, очищает промежуточные ресурсы через defer, а вызывающий код решает, нужно ли повторить операцию, показать сообщение или передать ошибку выше.
Вопрос: Вызывается ли deinit, если выбрасывающий инициализатор класса завершился ошибкой?
Ответ: Нет, потому что полностью инициализированный экземпляр не был создан. Очистку ресурсов, полученных до ошибки, нужно организовать внутри инициализатора или вспомогательных объектов с собственным управлением временем жизни.
Вопрос: Как сохранить причину ошибки, если инициализация вызывается через try??
Ответ: Никак: try? преобразует любой выброс в nil и удаляет различие между вариантами ошибки для этого уровня. Если причина нужна, следует использовать do-catch, передать ошибку дальше через throws или сохранить её в Result.
Вопрос: Может ли невыбрасывающий инициализатор удовлетворить требование протокола с выбрасывающим инициализатором?
Ответ: Да, если реализация гарантированно не выбрасывает ошибку. Обратная замена невозможна: вызывающий код, работающий с требованием без throws, не обязан обрабатывать ошибку, поэтому выбрасывающая реализация не может безопасно его подменить.