Функция возвращает именованный результат, а defer изменяет его перед выходом: какое значение получит вызывающий код?
Вызывающий код получит значение, изменённое в defer, если функция использует именованный возвращаемый результат. Перед выполнением defer выражение return уже вычисляется, но окончательная передача именованного результата вызывающему коду ещё не завершена.
Если defer изменяет обычную локальную переменную после return, это не меняет уже вычисленное возвращаемое значение.
defer предназначен для гарантированного выполнения завершающих действий при выходе из области видимости. Такой механизм решает проблему дублирования очистки ресурсов при нескольких путях выхода: обычном return, ошибке или досрочном завершении.
В Swift он особенно полезен рядом с throws, потому что позволяет централизовать освобождение ресурсов независимо от того, завершилась операция успешно или с ошибкой. Однако тело defer выполняется до фактического выхода, поэтому оно может повлиять на состояние, которое ещё будет возвращено.
Ошибочное предположение состоит в том, что после начала выполнения return функция уже полностью завершилась. На самом деле Swift сначала вычисляет возвращаемое выражение, затем выполняет отложенные блоки, и только после этого передаёт итоговое значение вызывающему коду.
Это создаёт риск незаметного изменения результата. Особенно опасна комбинация именованного результата и defer: изменение такого результата в defer действительно повлияет на значение, которое увидит вызывающий код.
У именованного возвращаемого результата есть имя, доступное внутри функции. При выполнении return значение присваивается этому результату, после чего запускаются блоки defer. Если один из них изменит именованный результат, изменённое значение будет возвращено.
У обычного локального значения поведение другое: выражение return value вычисляется заранее, и последующее изменение локальной переменной в defer уже не влияет на сохранённое значение.
В первом случае return result подготавливает возвращаемое значение, но defer затем увеличивает сам именованный результат. Во втором случае возвращаемое значение уже получено из value, поэтому дальнейшее изменение локальной переменной не учитывается.
Практическое правило: не изменяйте возвращаемый результат в defer без явной необходимости. Для очистки ресурсов, закрытия файлов, снятия блокировок и логирования лучше использовать независимые локальные значения. Такой код проще читать и сложнее случайно сломать при добавлении нового пути выхода.
Несколько блоков defer выполняются в обратном порядке добавления — последний зарегистрированный блок выполняется первым. Каждый блок относится к своей области видимости и запускается при выходе именно из неё.
Представим функцию, которая обновляет кэш и возвращает признак успешного обновления. Разработчик использует именованный результат и добавляет в defer обновление счётчика или финальный статус очистки. В результате служебная логика неожиданно меняет публичный результат операции.
Возможны три подхода:
defer — кратко, но намерение плохо видно и легко получить побочный эффект;return — поведение очевидно, но очистка и вычисление результата оказываются связанными;defer использовать только для завершения ресурса — немного больше кода, зато ниже риск скрытого изменения результата.Предпочтителен третий вариант. Например, функция может вычислить итоговый статус явно, а defer закрыть транзакцию или освободить блокировку, не обращаясь к возвращаемому значению. Это сохраняет предсказуемость API и упрощает тестирование.
Изменит ли defer значение, возвращаемое через обычное выражение return?
Нет, если изменяется обычная локальная переменная после того, как её значение уже использовано в return. Выражение возврата вычисляется до выполнения defer, поэтому последующая модификация локальной переменной не меняет подготовленный результат. Исключение — изменение самого именованного возвращаемого результата.
Что произойдёт при наличии нескольких defer в одной области видимости?
Они выполняются по принципу LIFO: последняя зарегистрированная операция выполняется первой. Это важно при создании вложенных ресурсов: освобождать обычно нужно сначала последний приобретённый ресурс, затем предыдущий. Нельзя рассматривать defer как список действий в порядке их записи.
Как влияет вложенная область видимости на выполнение defer?
defer внутренней области выполняется при выходе из этой области, даже если внешняя функция продолжает работу. Затем, при выходе из внешней области, выполняются её отложенные блоки. Поэтому при вложенных областях порядок определяется сначала границами областей видимости, а внутри каждой области — правилом LIFO.