Определи порядок вывода при вызове функции: когда срабатывает defer относительно обработки ошибки? пример с...

Определи порядок вывода при вызове функции: когда срабатывает defer относительно обработки ошибки?

import Foundation

enum Failure: Error { case invalid }

func run() throws -> Int {
    defer { print("defer") }
    print("start")
    throw Failure.invalid
}

do {
    _ = try run()
    print("success")
} catch {
    print("catch")
}
Проходите собеседования с ИИ помощником Hintsage

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

Вывод будет таким:

start defer catch

defer выполняется при выходе из области видимости функции, в том числе во время раскрутки стека из-за throw. До success выполнение не дойдет, потому что функция завершилась с ошибкой.

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

Механизм defer нужен для гарантированного освобождения ресурсов и выполнения завершающих действий независимо от способа выхода из области видимости: обычного return или передачи ошибки через throw.

Без такого механизма очистку пришлось бы дублировать перед каждым return и каждым путем обработки ошибки. Это повышает риск утечки ресурса, незакрытого файла или несогласованного состояния.

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

Функция сначала печатает start, затем выбрасывает ошибку. Оператор после throw в этой функции не выполняется, но передача управления вызывающему коду не происходит мгновенно: Swift сначала выполняет зарегистрированные блоки defer.

Неверное предположение, что defer срабатывает только при обычном возврате, приводит к ошибкам в очистке ресурсов. Обратная ошибка — считать defer обработчиком исключения: он выполняет завершающее действие, но сам по себе не заменяет catch и не предоставляет сведения об ошибке.

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

При входе в run Swift регистрирует блок defer. Затем функция выводит start и выполняет throw. Возвращаемое значение Int не формируется, поэтому выполнение сразу переходит к завершению текущей функции через ошибочный путь.

Перед тем как управление покинет run, выполняется ее defer, поэтому появляется defer. После этого ошибка передается в do-catch, где выполняется catch и печатается catch.

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

enum Failure: Error { case invalid } func load() throws { defer { print("close resource") } print("open resource") throw Failure.invalid } do { try load() } catch { print("handle error") }

Здесь ресурс условно открывается, затем при ошибке закрывается до входа в catch. Однако defer не гарантирует выполнение после аварийного завершения процесса, например при принудительном завершении приложения. Кроме того, слишком тяжелая логика в defer усложняет понимание момента изменения состояния и может скрывать дополнительные ошибки.

Если defer изменяет локальную переменную, нужно учитывать момент выполнения: блок запускается при выходе из области видимости. Для захвата значений существуют обычные правила замыканий Swift, поэтому нельзя бездумно считать, что defer сохраняет снимок всех переменных на момент объявления.

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

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

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

Выбранное решение — зарегистрировать удаление временного файла через defer сразу после его создания, а основную операцию оставить throws. Это гарантирует очистку при успешном return и при throw, а обработку причины ошибки можно централизовать на уровне вызывающего кода.

Результат — меньше дублирования, предсказуемое освобождение временного ресурса и сохранение типизированного потока ошибок. При этом в defer оставляют только надежную очистку, а не бизнес-логику и повторные операции, способные изменить итог обработки.

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

  1. Вопрос: В каком порядке выполняются два блока defer, объявленные в одной функции?

    Ответ: Они выполняются в обратном порядке объявления — по принципу LIFO. Если сначала зарегистрирован defer A, а затем defer B, при выходе получится порядок B, затем A. Это важно при работе с вложенными ресурсами: последний созданный ресурс обычно должен освобождаться первым.

  2. Вопрос: Выполнится ли defer внутри функции, если ошибка будет перехвачена во внешнем catch?

    Ответ: Да. defer привязан к области видимости функции, в которой он объявлен, а не к месту, где ошибка в итоге обрабатывается. Сначала выполняются отложенные действия при выходе из функции, затем управление получает внешний catch.

  3. Вопрос: Может ли defer отменить или заменить ошибку, выброшенную функцией?

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