В чём практическое отличие withCheckedContinuation от withUnsafeContinuation при адаптации callback API?

В чём практическое отличие withCheckedContinuation от withUnsafeContinuation при адаптации callback API?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

withCheckedContinuation проверяет соблюдение контракта продолжения: оно должно быть возобновлено ровно один раз. withUnsafeContinuation таких проверок не выполняет, поэтому оставляет больше ответственности разработчику и может быть немного дешевле, но ошибка приводит к неопределённому или трудно диагностируемому поведению.

Проверяемая гарантия касается только корректности использования continuation. Она не делает callback API потокобезопасным, не добавляет отмену и не предотвращает повторный вызов самого callback.

Исторический контекст

До появления async/await асинхронные операции в Swift часто представлялись callback-функциями. Continuation стала адаптером, который позволяет приостановить async-функцию и возобновить её из callback, не блокируя поток.

Такой адаптер создаёт новый риск: callback может не вызвать continuation, вызвать её несколько раз или вызвать после завершения связанной операции. Checked-вариант появился как средство обнаруживать нарушения этого контракта ближе к месту ошибки.

Постановка проблемы

Continuation представляет одно логическое продолжение выполнения, поэтому для обычной операции результат должен быть передан ровно один раз. Если callback не вызван, ожидающая задача останется приостановленной; если continuation возобновлена повторно, одна операция попытается завершить одну задачу несколько раз.

При использовании withUnsafeContinuation компилятор и runtime не дают такой диагностической защиты. Это особенно опасно при интеграции с legacy API, которое может вызвать callback из разных потоков, повторить его или сообщить одновременно об успехе и ошибке.

Подробное решение

withCheckedContinuation возвращает проверяемую continuation. Разработчик обязан вызвать resume ровно один раз по каждой ветке завершения: успех, ошибка или иной предусмотренный результат.

func load() async -> String { await withCheckedContinuation { continuation in LegacyAPI.fetch { value in continuation.resume(returning: value) } } } enum LegacyAPI { static func fetch(_ completion: @escaping (String) -> Void) { completion("ready") } }

Если callback не сработает, задача не завершится автоматически. Если callback сработает дважды, checked continuation обнаружит повторное возобновление во время выполнения; это диагностическая защита, а не механизм исправления ошибки.

withUnsafeContinuation уместна только тогда, когда разработчик доказал свойства внешнего API: callback вызывается обязательно, ровно один раз и в допустимый момент. Отсутствие проверок может уменьшить накладные расходы, но цена ошибки выше, а причина сбоя может проявиться далеко от адаптера.

Оба варианта не решают проблему отмены. Если операция должна прекращаться при отмене задачи, адаптеру нужен отдельный механизм отмены, например регистрация обработчика отмены и безопасное согласование его с callback. Также continuation сама по себе не сериализует callback и не защищает общее изменяемое состояние.

Ситуация из практики

Сетевой legacy-клиент иногда вызывает completion дважды: сначала сообщается промежуточный результат, затем финальный. Вариант с withUnsafeContinuation может привести к повреждению контракта async-функции без понятной диагностики. Вариант с withCheckedContinuation быстрее обнаружит дефект интеграции, но не устранит его.

Можно поставить флаг «уже завершено» под блокировкой и игнорировать последующие callback-вызовы. Это требует дополнительной синхронизации и аккуратного управления временем жизни ресурсов, зато позволяет безопасно адаптировать некорректный API.

Предпочтительное решение — исправить или обернуть legacy-клиент так, чтобы наружу поступал ровно один финальный callback, а затем использовать withCheckedContinuation. В результате ошибка контракта выявляется на границе адаптера, а не превращается в нестабильное поведение произвольной задачи.

Что кандидаты часто упускают

1. Что произойдёт, если callback вообще никогда не вызовет continuation?

Ожидающая async-задача останется приостановленной и не завершится сама. Checked-вариант не может определить, когда внешняя операция окончательно потеряна, поэтому для тайм-аута или отмены требуется отдельная логика. Нужно также согласовать завершение по тайм-ауту с возможным поздним callback, чтобы continuation всё равно была возобновлена ровно один раз.

2. Делает ли checked continuation callback безопасным при одновременных вызовах из разных потоков?

Нет. Она проверяет контракт самой continuation, но не превращает произвольный callback и окружающее состояние в безопасную конкурентную структуру. Если несколько потоков могут завершить операцию одновременно, выбор победившего завершения и очистку ресурсов нужно защищать синхронизацией или изолировать подходящим механизмом Swift Concurrency.

3. Когда допустимо выбрать withUnsafeContinuation?

Только когда инварианты внешнего API надёжно известны и контролируются: завершение гарантировано, происходит ровно один раз, а время вызова не нарушает жизненный цикл операции. Такой выбор может убрать проверочные накладные расходы в чувствительном к производительности коде, но обычно на этапе разработки безопаснее использовать checked-вариант и переходить к unsafe лишь после обоснования измерениями и контрактом API.