defer — управляющая конструкция в Swift, которая планирует выполнение блока кода на момент выхода из текущей области видимости (scope). Блок defer выполняется независимо от способа завершения — return, break, throw, fatalError или нормальное завершение. По данным Swift Language Guide (2025), при наличии нескольких defer в одном scope они выполняются в обратном порядке объявления — последний объявленный выполняется первым (LIFO). Это делает defer незаменимым для гарантированной очистки ресурсов: закрытия файловых дескрипторов, снятия блокировок, освобождения временных указателей без риска пропустить cleanup при досрочном выходе.
Главное
defer — это управляющая конструкция языка Swift, введённая в Swift 2.0 (2015), которая откладывает выполнение своего блока до момента, когда текущий scope завершается. Ключевая особенность: defer гарантирует выполнение своего тела независимо от того, как именно завершается scope — успешно (return), с ошибкой (throw), преждевременно (break, continue) или фатально (fatalError, precondition).
Синтаксически defer выглядит как defer { /* код */ } и может располагаться в любом месте внутри scope. Компилятор Swift гарантирует, что код внутри defer будет выполнен, даже если между объявлением defer и концом scope произойдёт исключение или return. Это принципиально отличает defer от обычного кода, размещённого в конце функции, который может быть пропущен при досрочном выходе.
По данным статьи Криса Латтнера (Swift Creator, 2015), defer был вдохновлён аналогичными конструкциями в других языках — defer в Go, finally в Java/Python, scope guard в C++ — но с важным отличием: в Swift defer выполняется в конце scope, а не сразу после try-catch-блока. Это даёт более предсказуемое поведение для cleanup в функциях с множественными точками выхода.
Используйте defer для симметричного управления ресурсами: открытие файла → defer { close }, установка блокировки → defer { unlock }. Такой паттерн гарантирует, что освобождение ресурса не будет пропущено ни при каких обстоятельствах.
Когда в одном scope объявлено несколько defer, они выполняются в обратном порядке объявления (LIFO — Last In, First Out). Это означает, что последний объявленный defer выполнится первым, а первый — последним:
func exampleDeferOrder() {
defer { print("First defer") }
defer { print("Second defer") }
defer { print("Third defer") }
print("Function body")
}
// Output:
// Function body
// Third defer
// Second defer
// First defer
Порядок LIFO важен для корректного управления вложенными ресурсами. Если сначала открывается файл A, затем файл B, освобождать их нужно в обратном порядке: сначала B, потом A. С defer это происходит автоматически — объявите defer сразу после открытия каждого ресурса, и порядок очистки будет правильным независимо от числа точек выхода из функции.
По данным Swift by Sundell (2024), эта особенность делает defer идеальным для вложенных блокировок и транзакций: захват блокировки → defer { unlock } → захват следующей → defer { unlock }. LIFO гарантирует, что блокировки освобождаются в порядке, обратном захвату, предотвращая дедлоки.
Основное применение defer — гарантированная очистка ресурсов. Рассмотрим работу с файловой системой. Открытие файла через FileHandle требует явного закрытия — defer гарантирует, что close будет вызван при любом сценарии:
func readFile(path: String) throws -> String {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { try? handle.close() }
let data = try handle.readToEnd()
guard let data else { throw FileError.empty() }
return String(data: data, encoding: .utf8) ?? ""
// handle.close() will be called even on throw or return
}
Другой типичный сценарий — UI-анимации с флагом загрузки. Перед началом загрузки устанавливается флаг isLoading = true, а defer меняет его на false при выходе из функции, независимо от успеха или ошибки запроса. Это предотвращает ситуацию, когда флаг остаётся true из-за необработанной ошибки и блокирует интерфейс навсегда.
По данным Bitbucket Engineering Blog (2024), defer также применяется для профилирования: в начале функции можно записать время, а в defer — вычислить и вывести разницу. Это даёт точные замеры производительности всех путей выполнения, включая ошибочные.
defer эффективно сочетается с throws-функциями. Когда функция может выбросить ошибку на любом этапе, defer гарантирует cleanup без дублирования кода в каждом catch-блоке или guard-раннем выходе:
func processTransaction() throws {
let db = try openDatabase()
defer { closeDatabase(db) }
let user = try fetchUser(from: db)
defer { logAudit(user) }
let result = try performPayment(user)
sendNotification(result)
// closeDatabase(db) and logAudit(user) will be called
// on any throw or return
}
Важно: defer выполняется до передачи управления из catch-блока, но после возникновения ошибки. Если в defer выброшена ошибка, Swift не разрешает использовать try внутри defer напрямую — требуется try? или try!. По данным Apple Documentation, Swift не позволяет ошибке «вылететь» из defer, поскольку это нарушило бы гарантию выполнения блока.
Располагайте defer сразу после захвата ресурса. Это следует принципу близости: читатель видит захват и освобождение рядом, что повышает надёжность кода и упрощает код-ревью.
defer выполняется при выходе из scope, в котором он объявлен. Если defer объявлен внутри do-блока, он выполняется при выходе из этого блока, а не из внешней функции. Если внутри for-цикла — при каждой итерации:
func scopeExample() {
print("start")
do {
defer { print("do-block defer") }
print("inside do")
}
// "do-block defer" prints here
print("after do")
}
// Output: start, inside do, do-block defer, after do
for i in 1...3 {
defer { print("end iteration \(i)") }
print("iteration \(i)")
}
// Output: iteration 1, end iteration 1, iteration 2, end iteration 2, ...
Переменные, захваченные defer, читаются на момент выхода из scope, а не на момент объявления defer. Если переменная изменяется между объявлением defer и концом scope, defer увидит последнее значение. Это важное отличие от замыканий, где захват происходит на момент создания. Будьте внимательны: изменения переменной после объявления defer отразятся на его выполнении.
Первая ошибка — предположение о порядке выполнения, отличном от LIFO. Если порядок cleanup важен, а defer объявлены в неправильном порядке, ресурсы могут освобождаться с нарушением зависимости. Решение: объявляйте defer сразу после захвата каждого ресурса. Второй ресурс открыт → defer { close second } до того, как закрыт первый.
Вторая ошибка — использование defer для логики, не связанной с очисткой. defer предназначен для гарантированного cleanup, а не для основного потока управления. Если код в defer влияет на возвращаемое значение, это почти всегда ошибка. defer не может изменить return-значение функции (в отличие от Java finally, где return в finally перезаписывает исходный return).
Третья ошибка — выброс ошибки из defer. Swift запрещает try внутри defer, если ошибка может распространиться наружу. Используйте try? или try! для операций, которые могут выбросить ошибку, или оборачивайте их в отдельную функцию без throws. По данным O'Reilly «Swift in Depth» (2025), хорошая практика — сделать cleanup-функции небросающими (non-throwing) или обрабатывать ошибки внутри defer.
Часто задаваемые вопросы
defer — управляющая конструкция Swift, откладывающая выполнение блока до выхода из текущей области видимости. Блок выполняется всегда — при return, throw, break или нормальном завершении. Используется для гарантированной очистки ресурсов: закрытия файлов, снятия блокировок.
В обратном порядке объявления (LIFO) — последний объявленный defer выполняется первым. Это гарантирует корректный cleanup вложенных ресурсов: если ресурс B открыт после A, он будет закрыт до A, что предотвращает зависимости от уже освобождённых ресурсов.
Не напрямую — Swift запрещает распространение ошибки из defer. Используйте try? или try! для операций, которые могут выбросить ошибку. Лучшая практика — делать cleanup-функции non-throwing или обрабатывать ошибки внутри defer без проброса наружу.
defer привязан к scope и выполняется при любом выходе, включая return, throw и break. finally (в других языках) привязан к try-catch и выполняется только при наличии try. В Swift нет finally — defer полностью покрывает этот сценарий и работает для любого scope, а не только для обработки ошибок.
Да, defer читает переменные на момент выхода из scope, а не на момент объявления. Если переменная изменяется после объявления defer, блок defer увидит последнее значение. Это отличается от обычных замыканий, где захват фиксируется при создании.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также