При передаче замыкания в функцию без @escaping его пытаются сохранить для вызова после завершения функции. Какой механизм Swift запрещает это и почему?
Swift запрещает сохранять неэкранирующее замыкание (non-escaping) после завершения функции, потому что оно гарантированно используется только во время текущего вызова. Для отложенного вызова замыкание нужно явно объявить с атрибутом @escaping.
Замыкания часто используются как синхронные обработчики коллекций или как отложенные callbacks. Swift разделяет эти сценарии, чтобы API явно сообщал, может ли замыкание пережить вызов функции.
По умолчанию замыкание считается неэкранирующим. Такой выбор делает обычный синхронный случай безопаснее и позволяет компилятору не учитывать сохранение замыкания во внешнем состоянии.
Если функция сохранит неэкранирующее замыкание в свойство или передаст его объекту для последующего вызова, замыкание станет доступно после выхода из функции. Это нарушает контракт параметра: компилятор ожидал, что замыкание завершит работу до возврата из функции.
Ошибка проявляется на этапе компиляции, а не во время выполнения. Нельзя исправлять её простым приведением типа: нужно изменить контракт функции и явно указать, что замыкание может escaping.
Параметр без @escaping можно вызвать непосредственно внутри функции, но нельзя сохранить в переменную, свойство или передать в другой долгоживущий объект. Атрибут @escaping разрешает замыканию пережить вызов функции и обычно означает, что оно может быть вызвано позже, например после завершения сетевого запроса.
В setAction замыкание сохраняется в свойстве, поэтому параметр обязан быть @escaping. В runNow замыкание вызывается синхронно и не сохраняется, поэтому атрибут не нужен.
@escaping не означает, что замыкание обязательно будет вызвано позже или что оно будет вызвано многократно. Атрибут только разрешает сохранение; фактическое поведение определяет реализация функции.
Экранирующее замыкание может захватывать значения и объекты, продлевая их время жизни. Поэтому при работе с классами нужно учитывать циклы сильных ссылок и при необходимости использовать слабый захват, но это отдельный вопрос от самого права сохранять замыкание.
В менеджере загрузки нужно сохранить completion-обработчик и вызвать его после получения ответа. Вариант с параметром без @escaping не компилируется, поскольку обработчик сохраняется в свойстве.
Можно было бы выполнить обработчик сразу, но это меняет семантику API и не подходит для асинхронной загрузки. Можно также передать обработчик напрямую в сетевой слой, если тот уже принимает @escaping, но менеджеру всё равно нужен соответствующий контракт при сохранении.
Выбранное решение — объявить параметр @escaping, явно описав асинхронную семантику. После этого код компилируется, а при проектировании класса отдельно проверяется отсутствие циклической ссылки между загрузчиком и замыканием.
@escaping, что замыкание обязательно выполнится после возврата из функции?Нет. @escaping лишь разрешает сохранить замыкание и вызвать его позже. Реализация может вызвать его синхронно, асинхронно, несколько раз или не вызвать вовсе, если это допускает контракт API.
@escaping?Да, такой вызов обычно возможен: внешняя функция может вызвать переданное замыкание и сохранить его. При этом безопасность определяется тем, что исходное замыкание существует как минимум на время текущего вызова; функция, принимающая @escaping, не должна сохранять его за пределами разрешённого контракта без корректного преобразования и условий вызова.
@escaping может привести к проблеме с памятью?Сохраняемое замыкание удерживает захваченные значения по правилам захвата. Если объект хранит замыкание, а замыкание сильно захватывает этот же объект, возникает цикл сильных ссылок, и объект не освобождается. Для разрыва цикла применяют слабый или незахватывающий способ ссылки, если жизненный цикл объекта это допускает.