Перейти к содержимому
Теория · Ошибки · урок 6 из 7

defer

Вечером сотрудник почты закрывает своё окошко, как бы ни прошла смена: разобрал все посылки или последнюю вернул со штампом. Чтобы не забыть, он с утра, едва открыв окошко, вешает на него записку «уходя, закрыть». Записку читают на выходе, каким бы путём ни уходили.

У функции тоже бывает несколько выходов:

  • return в конце;
  • return в ветке guard;
  • throw.

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

SWIFT
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

Давайте разберём программу по шагам:

  1. print("open", terminator: " ") печатает open — окошко открылось.
  2. defer { … } вешает записку. Код в фигурных скобках сейчас не выполняется: он отложен до выхода из функции.
  3. В вызове parse("3") функция выходит через return n. Перед самым выходом срабатывает defer и печатает close. Только потом print снаружи печатает результат 3.
  4. В вызове parse("x") функция выходит через throw, и defer всё равно срабатывает. Ошибку try? превращает в nil, а ?? 0 даёт 0.

Когда срабатывает defer

Код из defer выполняется при выходе из блока в фигурных скобках, где defer написан. Неважно, как выходят:

  • через return;
  • через throw;
  • когда кончились строки блока.

Блок — это не только тело функции. defer в теле цикла срабатывает в конце каждого круга, а defer в блоке do — при выходе из do.

Несколько defer

В одном блоке может стоять несколько defer. Тогда они срабатывают в обратном порядке: последний написанный выполняется первым.

SWIFT
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

Давайте пройдём по функции:

  1. Сотрудник отпирает дверь и вешает записку «запереть».
  2. Он включает лампу и вешает вторую записку — «выключить лампу».
  3. Он работает: печатается work.
  4. На выходе записки читаются с последней: сначала lamp off, потом lock.

Обратный порядок удобен: что открыли последним, закрывают первым. Лампу гасят, пока дверь ещё не заперта.

Этот вопрос часто задают на собеседовании: когда выполняется defer и в каком порядке? Ответ:

  • при выходе из блока, где он написан: через return, через throw или в конце блока;
  • не на своей строке, а после всех остальных строк блока;
  • несколько defer выполняются в обратном порядке: последний написанный — первым.

Здесь картинка с запиской неточна. Записку на окошке видно весь день, и выполнить её можно когда угодно. defer раньше времени не выполнить: он срабатывает только на выходе из блока и только если программа успела дойти до его строки.

Частая ошибка

defer написали ниже строки, которая может выйти из функции:

SWIFT
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 срабатывает, только если выполнение дошло до его строки, поэтому его пишут до первого возможного выхода.