Программирование SwiftSwift CoreРазработчик приложений на Swift

Функция содержит несколько defer в одном и вложенных блоках: как Swift определяет порядок их выполнения при...

Функция содержит несколько defer в одном и вложенных блоках: как Swift определяет порядок их выполнения при выходе из области видимости?

Проходите собеседования с ИИ помощником Hintsage

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

defer выполняется при выходе из области видимости, независимо от того, произошёл обычный возврат или было выброшено исключение. Несколько блоков defer в одной области выполняются в обратном порядке объявления, то есть по принципу LIFO; вложенная область завершается раньше внешней.

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

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

Такой подход делает завершающие действия локальными и заметными рядом с кодом, который захватывает ресурс. Он не отменяет автоматическое управление памятью, но помогает управлять ресурсами и состоянием, для которых одного ARC недостаточно.

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

Без defer одинаковую очистку пришлось бы повторять перед каждым вариантом завершения функции. При добавлении нового return разработчик может забыть обновить один из путей, из-за чего останется заблокированный ресурс, незавершённая транзакция или некорректное временное состояние.

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

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

defer привязан к конкретной области видимости. При выходе из неё Swift выполняет отложенные блоки в порядке, обратном их объявлению. При переходе из вложенной области сначала выполняются её отложенные действия, затем — действия внешних областей.

Выражение для return вычисляется до выполнения defer, но фактический выход из функции завершается после выполнения отложенных блоков. Поэтому defer может изменить внешнее состояние, однако не изменит уже вычисленное возвращаемое значение, если это значение не является ссылкой на изменяемый объект.

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

func makeLog() -> String { var log = [String]() defer { log.append("outer") } do { defer { log.append("inner") } log.append("work") } return log.joined(separator: ",") } print(makeLog()) // work,inner,outer

В примере внутренняя область завершается первой, поэтому её defer добавляет inner раньше внешнего defer. Возвращаемая строка вычисляется после выхода из do, но до выполнения внешнего defer, поэтому в неё попадает только work,inner? Нет: выражение log.joined вычисляется непосредственно перед выходом из функции, когда внутренний блок уже завершён, но внешний defer ещё не выполнен. Следовательно, фактический результат — work,inner, а добавление outer происходит уже после формирования возвращаемой строки.

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

Функция получает блокировку, изменяет общее состояние и должна освободить блокировку при успешном завершении, раннем return и ошибке. Вариант с ручным вызовом освобождения в каждом месте короток для двух путей, но плохо масштабируется: новый путь легко оставить без очистки.

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

Практичнее захватить ресурс и сразу объявить defer для его освобождения. Тогда блокировка освобождается при любом нормальном выходе из области и при распространении ошибки, а порядок нескольких очисток можно явно задать обратным порядком объявления. Важно не помещать в такой блок действия, которые могут требовать уже освобождённого ресурса.

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

  1. Вопрос: Изменится ли возвращаемое значение, если defer изменяет локальную переменную, использованную в выражении return?

    Ответ: Обычно нет. Выражение возврата вычисляется до запуска defer, поэтому возвращается уже вычисленное значение. Исключение по наблюдаемому эффекту возможно, если возвращается ссылка на изменяемый объект: defer может изменить сам объект, и вызывающий код увидит это изменение через полученную ссылку.

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

    Ответ: Сначала выполняются все отложенные действия внутренней области в обратном порядке объявления. После этого выполняются defer внешней области, также в обратном порядке. Это позволяет строить очистку слоями: сначала закрыть зависимый ресурс, затем освободить ресурс, от которого он зависит.

  3. Вопрос: Что произойдёт с defer, если функция завершает работу из-за необработанной ошибки?

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