defer
Вечером сотрудник почты закрывает своё окошко, как бы ни прошла смена: разобрал все посылки или последнюю вернул со штампом. Чтобы не забыть, он с утра, едва открыв окошко, вешает на него записку «уходя, закрыть». Записку читают на выходе, каким бы путём ни уходили.
У функции тоже бывает несколько выходов:
returnв конце;returnв веткеguard;throw.
Если перед каждым выходом нужно сделать одно и то же, рано или поздно один из выходов забудут. Для этого в Swift есть defer:
enum ParseError: Error {
case badFormat
}
func parse(_ text: String) throws -> Int {
print("open", terminator: " ")
defer { print("close", terminator: " ") }
guard let n = Int(text) else {
throw ParseError.badFormat
}
return n
}
print((try? parse("3")) ?? 0) // open close 3
print((try? parse("x")) ?? 0) // open close 0Давайте разберём программу по шагам:
print("open", terminator: " ")печатаетopen— окошко открылось.defer { … }вешает записку. Код в фигурных скобках сейчас не выполняется: он отложен до выхода из функции.- В вызове
parse("3")функция выходит черезreturn n. Перед самым выходом срабатываетdeferи печатаетclose. Только потомprintснаружи печатает результат3. - В вызове
parse("x")функция выходит черезthrow, иdeferвсё равно срабатывает. Ошибкуtry?превращает вnil, а?? 0даёт0.
Когда срабатывает defer
Код из defer выполняется при выходе из блока в фигурных скобках, где defer написан. Неважно, как выходят:
- через
return; - через
throw; - когда кончились строки блока.
Блок — это не только тело функции. defer в теле цикла срабатывает в конце каждого круга, а defer в блоке do — при выходе из do.
Несколько defer
В одном блоке может стоять несколько defer. Тогда они срабатывают в обратном порядке: последний написанный выполняется первым.
func shift() {
print("unlock", terminator: " ")
defer { print("lock") }
print("lamp on", terminator: " ")
defer { print("lamp off", terminator: " ") }
print("work", terminator: " ")
}
shift() // unlock lamp on work lamp off lockДавайте пройдём по функции:
- Сотрудник отпирает дверь и вешает записку «запереть».
- Он включает лампу и вешает вторую записку — «выключить лампу».
- Он работает: печатается
work. - На выходе записки читаются с последней: сначала
lamp off, потомlock.
Обратный порядок удобен: что открыли последним, закрывают первым. Лампу гасят, пока дверь ещё не заперта.
Этот вопрос часто задают на собеседовании: когда выполняется defer и в каком порядке? Ответ:
- при выходе из блока, где он написан: через
return, черезthrowили в конце блока; - не на своей строке, а после всех остальных строк блока;
- несколько
deferвыполняются в обратном порядке: последний написанный — первым.
Здесь картинка с запиской неточна. Записку на окошке видно весь день, и выполнить её можно когда угодно. defer раньше времени не выполнить: он срабатывает только на выходе из блока и только если программа успела дойти до его строки.
Частая ошибка
defer написали ниже строки, которая может выйти из функции:
func late(_ text: String) throws -> Int {
print("open", terminator: " ")
guard let n = Int(text) else {
throw ParseError.badFormat
}
defer { print("close", terminator: " ") } // ✗
return n
}Программа компилируется, но для текста не из цифр close не напечатается. Функция выйдет через throw раньше, чем дойдёт до строки с defer, и не успеет повесить записку. Пишите defer сразу после того, что он должен закрыть, и до первой строки, которая может выйти из функции.
Главное
deferоткладывает код до выхода из блока, где он написан: черезreturn, черезthrowили в конце блока.- Код из
deferвыполняется после всех остальных строк блока, а не на своей строке. - Несколько
deferвыполняются в обратном порядке: последний написанный — первым. deferсрабатывает, только если выполнение дошло до его строки, поэтому его пишут до первого возможного выхода.