Разберите механизм временного преобразования non-escaping-замыкания в escaping: может ли такой вызов продлить жизнь захваченного объекта после завершения withoutActuallyEscaping?
func accept(_ action: @escaping () -> Void) {
action()
}
func run(_ action: () -> Void) {
withoutActuallyEscaping(action) { temporary in
accept(temporary)
}
}
final class Probe {
func touch() { print("touch") }
deinit { print("deinit") }
}
func test() {
let probe = Probe()
run { probe.touch() }
}
test()
withoutActuallyEscaping не превращает замыкание в действительно escaping и не гарантирует продление жизни захваченного объекта после завершения переданного ему тела. Замыкание можно использовать как escaping только внутри этого тела; после его возврата оно не должно сохраняться или выполняться.
В примере accept вызывает замыкание синхронно, поэтому использование корректно. Точный момент вызова deinit не следует определять только по концу функции: ARC может освободить объект после последнего необходимого использования, но withoutActuallyEscaping сам по себе не добавляет долгожившего владельца.
В Swift замыкания по умолчанию являются non-escaping. Такой контракт позволяет компилятору не учитывать их возможное хранение после возврата функции и помогает эффективнее управлять временем жизни захваченных значений.
Иногда синхронному API внутри реализации нужен параметр типа @escaping, хотя фактически он используется только во время текущего вызова. withoutActuallyEscaping решает именно эту задачу, не ослабляя внешний контракт функции.
Ошибка возникает, когда временно преобразованное замыкание сохраняют в свойстве, передают в асинхронную задачу или вызывают после возврата из тела withoutActuallyEscaping. В этот момент замыкание и его захваты уже не обязаны существовать.
Если замыкание сильно захватывает объект, неправильное сохранение может привести к обращению к недействительному состоянию, неопределённому поведению или к ошибочному ожиданию, что объект будет жить дольше. Сам факт типа @escaping внутри замыкания не означает долгосрочного владения.
withoutActuallyEscaping предоставляет временное представление non-escaping-замыкания как @escaping. Это представление действительно только во время выполнения переданного тела. API, вызываемый внутри тела, может принимать @escaping, но не должен сохранять полученное замыкание после возврата.
Вызов accept(temporary) корректен: функция принимает escaping-параметр, но немедленно вызывает его и ничего не сохраняет. Если бы accept записывал action в глобальную переменную или запускал его асинхронно, контракт withoutActuallyEscaping был бы нарушен.
Сильные захваты замыкания участвуют в ARC только в пределах фактического времени жизни самого замыкания. withoutActuallyEscaping не является заменой withExtendedLifetime: он не предоставляет отдельную гарантию сохранения объекта после выхода из тела.
Точный момент deinit также не следует связывать с концом лексической области. ARC ориентируется на необходимость ссылок и допускает оптимизацию времени их освобождения. Если требуется явная гарантия жизни объекта до определённого участка кода, применяют withExtendedLifetime, а не withoutActuallyEscaping.
Допустим, синхронный адаптер принимает обычное non-escaping-замыкание, а используемый внутри универсальный компонент объявлен с @escaping. Вариант с withoutActuallyEscaping позволяет переиспользовать компонент без хранения callback: это сохраняет безопасный внешний контракт и не создаёт искусственного долгоживущего владельца.
Альтернатива — изменить внешний параметр на @escaping. Она проще, но расширяет контракт: вызывающий код должен учитывать возможность хранения замыкания, а его сильные захваты могут дольше удерживать объекты и создавать циклы ссылок.
Другая альтернатива — скопировать логику вызова вручную. Это устраняет ограничение типов, но ведёт к дублированию и хуже масштабируется. Поэтому выбранный вариант подходит, если вызов строго синхронный и адаптер гарантированно не сохраняет замыкание; результатом будет отсутствие скрытого продления времени жизни после завершения адаптации.
Можно ли передать временное escaping-замыкание в Task или DispatchQueue.async?
Нет, если оно будет выполнено после выхода из тела withoutActuallyEscaping. Такие API сохраняют замыкание для будущего выполнения, поэтому это нарушает требование о том, что замыкание не должно реально escaping. Для асинхронного запуска параметр должен изначально быть @escaping, а время жизни захватов нужно проектировать отдельно.
Продлевает ли сильный захват объекта время его жизни до конца функции-владельца?
Не обязательно. Сильный захват действительно удерживает объект, пока живо замыкание, но ARC может уничтожить замыкание и освободить его захваты после последнего использования. Для точной гарантии границы жизни применяют withExtendedLifetime или явное управление владельцем, а не полагаются на видимую область переменной.
Что произойдёт, если преобразованное замыкание сохранить, но вызвать только иногда?
Это всё равно нарушение контракта: важен сам факт выхода замыкания за допустимую динамическую область, а не наличие последующего вызова. Поведение такого кода не следует считать безопасным или исправным даже в случае, когда сохранённое замыкание фактически никогда не запускается.