Нужно передать неэкранирующее замыкание синхронному API, который принимает экранирующее. Какой механизм Swift позволяет сделать это без продления времени жизни замыкания?
Используйте withoutActuallyEscaping. Он временно позволяет передать неэкранирующее замыкание функции, ожидающей экранирующее, но не продлевает его время жизни.
Этот механизм безопасен только при гарантии, что переданный API фактически не сохранит замыкание и не вызовет его после завершения блока withoutActuallyEscaping.
В Swift замыкания по умолчанию являются неэкранирующими: компилятор предполагает, что они используются только во время текущего вызова функции. Это позволяет строже контролировать время жизни захваченных значений и дает компилятору больше возможностей для оптимизации.
Некоторые API обязаны принимать экранирующие замыкания, потому что сохраняют их для последующего вызова. Из-за различия контрактов обычное неэкранирующее замыкание нельзя напрямую передать такому API. withoutActuallyEscaping появился как точечный адаптер для случаев, когда API формально требует экранирующий тип, но фактическое использование остается синхронным.
Предположим, вспомогательная функция принимает замыкание как @escaping, хотя вызывает его немедленно и не сохраняет. Передача ей неэкранирующего замыкания напрямую запрещена: это могло бы нарушить гарантию, что замыкание не переживет вызывающую функцию.
Простая замена параметра на @escaping решает ошибку типов, но меняет контракт всей функции. После этого вызывающий код должен считать, что замыкание потенциально сохраняется, а захваченные объекты могут жить дольше ожидаемого.
withoutActuallyEscaping создает временное представление неэкранирующего замыкания как экранирующего внутри специального блока. После выхода из этого блока исходная гарантия восстанавливается: замыкание не должно использоваться позже.
В примере invokeSynchronously формально принимает @escaping, но вызывает замыкание до возврата. Поэтому адаптация допустима. Сам withoutActuallyEscaping не превращает замыкание в действительно долгоживущее и не делает безопасными асинхронный вызов, сохранение в свойстве или передачу в очередь.
Если замыкание фактически выйдет за пределы блока, поведение нарушит контракт withoutActuallyEscaping и может привести к аварийному завершению во время выполнения. Поэтому этот механизм нельзя применять как способ обойти ограничения для DispatchQueue, делегатов, подписок или любых API, которые сохраняют callback.
Предпочтительный порядок решений такой: сначала изменить собственный API на неэкранирующий, если сохранение не нужно; затем использовать withoutActuallyEscaping для существующего синхронного экранирующего API; и только при реальном долгосрочном хранении объявлять замыкание @escaping и явно учитывать его время жизни.
В библиотеке есть синхронный адаптер, который принимает обработчик через общий интерфейс с параметром @escaping. Обработчик не сохраняется, но изменить публичный интерфейс нельзя.
Вариант с объявлением вызывающей функции как @escaping прост, однако он ослабляет контракт и может скрыть ошибочное сохранение замыкания. Передача напрямую не компилируется. Асинхронная обертка еще хуже: она действительно делает время вызова независимым от текущей функции.
Выбран withoutActuallyEscaping, поскольку фактическая реализация вызывает обработчик внутри того же стека вызовов и не сохраняет его. В результате интерфейс библиотеки остается неизменным, а компилятор и ревью кода сохраняют четкое требование: блок адаптации должен быть полностью синхронным.
withoutActuallyEscaping время жизни захваченных объектов?Нет. Он только предоставляет временное представление с другим типом вызова. Время жизни захваченных значений по-прежнему связано с исходным неэкранирующим замыканием и областью его безопасного использования.
Нет. Если асинхронная операция сохранит callback и вызовет его после завершения блока, нарушится основной контракт механизма. withoutActuallyEscaping не является способом безопасно передавать локальные неэкранирующие замыкания в очередь, таймер или подписку.
@escaping, чтобы устранить ограничение?@escaping меняет семантический контракт: функция получает право хранить замыкание после возврата. Это может продлить жизнь захваченных объектов, создать циклы удержания и усложнить анализ потоков данных. Если замыкание реально используется только синхронно, неэкранивающий параметр точнее описывает API, а временная адаптация должна быть локальной и явно обоснованной.