Объясните механизм: зачем Swift помечает замыкание как escaping, если оно может быть вызвано после возврата функции?
escaping-замыкание может пережить вызвавшую его функцию: оно сохраняется, возвращается или передаётся коду, который выполнит его позже. Поэтому Swift требует явно указать @escaping: это контракт, предупреждающий о более длительном времени жизни замыкания и его захваченных данных.
Замыкание захватывает необходимые локальные переменные. Если захваченная переменная должна изменяться после выхода из функции, Swift сохраняет доступное замыканию общее хранилище для этой переменной, поэтому её время жизни продлевается.
Замыкания возникли как способ передавать поведение так же удобно, как данные. Это позволяет отделить момент настройки операции от момента её выполнения — например, передать обработчик события или завершения асинхронной задачи.
Такой подход создаёт проблему времени жизни: локальные переменные обычно существуют только во время вызова функции, а переданное замыкание может быть выполнено позже. Swift разделяет обычные и escaping-замыкания, чтобы сделать это различие проверяемым на этапе компиляции.
По умолчанию параметр-замыкание считается non-escaping. Компилятор может предполагать, что оно завершит выполнение до возврата из функции, поэтому его нельзя сохранить для последующего вызова или вернуть наружу.
Если нарушить это предположение, замыкание может обратиться к уже недействительным локальным данным. В Swift такая ситуация не допускается неявно: параметр нужно объявить с @escaping, а разработчик должен учитывать продлённое время жизни захваченных объектов и возможные циклы удержания.
Аннотация @escaping относится к параметру функции и означает, что переданное замыкание может покинуть область выполнения этой функции. Оно может быть сохранено в свойстве, помещено в очередь, возвращено из другой функции или передано другой escaping-операции.
Пример показывает, что локальная переменная продолжает существовать после возврата prepare, поскольку сохранённое замыкание всё ещё захватывает её:
Здесь message не уничтожается сразу после завершения prepare. Концептуально Swift предоставляет замыканию общее захваченное хранилище; точное внутреннее представление не является частью прикладного контракта языка.
@escaping не означает, что замыкание обязательно будет вызвано асинхронно. Его можно вызвать и немедленно, но сам API получает право сохранить его дольше текущего вызова. Для non-escaping-замыкания компилятор имеет более строгие гарантии времени жизни и может применять дополнительные оптимизации.
Захват значения не равен копированию всего графа объектов. Если замыкание захватывает экземпляр класса, обычно удерживается ссылка на тот же экземпляр; изменение его свойств будет видно другим ссылкам. У изменяемой локальной переменной замыкания обычно разделяют одно захваченное хранилище, поэтому несколько вызовов могут наблюдать и изменять одно состояние.
Захваченные объекты могут удерживаться до уничтожения замыкания. Если объект хранит замыкание, а замыкание сильно захватывает этот же объект, возникает цикл сильных ссылок. Для разрыва цикла применяют список захвата с weak или unowned, но выбор зависит от гарантии времени жизни: weak даёт необязательную ссылку, а unowned предполагает, что объект всё ещё существует.
Представьте экран iOS, который сохраняет callback сетевого запроса. Callback захватывает сам экран, а объект запроса хранится экраном до завершения операции. При отсутствии отмены или слабого захвата экран может не освободиться после закрытия: экран удерживает запрос, запрос удерживает callback, callback удерживает экран.
Вариант с сильным захватом прост и безопасен относительно преждевременного уничтожения объекта, но может привести к утечке до завершения операции. Безусловно использовать unowned опасно: если callback сработает после уничтожения экрана, обращение приведёт к аварийному завершению.
Практичное решение — использовать weak для владельца экрана и дополнительно отменять запрос при уходе экрана. weak предотвращает цикл и позволяет callback тихо ничего не делать, если экран уже уничтожен; отмена уменьшает лишнюю работу и количество ожидающих замыканий.
Нет. @escaping описывает возможность пережить текущий вызов, а не способ планирования. Функция может сохранить замыкание и вызвать его сразу же, позже в том же потоке или асинхронно в другом потоке. Асинхронность определяется реализацией API, а @escaping — только допустимым временем жизни.
Замыкание захватывает не обязательно отдельную копию значения на каждый вызов. Для изменяемой локальной переменной создаётся общее захваченное состояние, доступное функции и замыканию после выхода из исходной функции. Поэтому последовательные вызовы могут видеть результат предыдущих изменений; при многопоточном доступе к такому состоянию дополнительно нужна синхронизация.
weak, а когда unowned?weak применяют, когда захваченный объект может быть уничтожен раньше замыкания. Ссылка автоматически становится nil, поэтому вызов требует безопасной проверки, зато не вызывает аварийного завершения из-за времени жизни.
unowned подходит только при доказуемой гарантии, что объект переживёт замыкание. Он не увеличивает время жизни объекта, но обращение после его уничтожения является ошибкой выполнения. Если такой инвариант трудно доказать, weak безопаснее, хотя он требует обработки отсутствующего объекта.