Continuations в Swift: как безопасно связать callback-API с async/await
С выходом async/await в Swift асинхронный код стал значительно чище и понятнее. Однако в реальных проектах по-прежнему используется большое количество API, построенных на completion-handler’ах, делегатах и callback’ах.
Полностью переписать такие API не всегда возможно или целесообразно. Для таких случаев в Swift существуют continuations — официальный механизм для связывания старого callback-кода с новой моделью async/await.
В статье разберём:
- что такое continuations и зачем они нужны;
- какие виды continuations существуют в Swift;
- как и когда использовать withCheckedThrowingContinuation;
- какие ошибки встречаются чаще всего;
- как корректно обрабатывать отмену Task.
Что такое continuation
Continuation — это объект, который позволяет вручную возобновить выполнение async-функции.
Когда Swift выполняет код вида:
он приостанавливает текущую задачу и ждёт, пока loadValue() завершится. Если внутри loadValue() используется callback-based API, Swift не знает, когда и с каким результатом нужно продолжить выполнение.
В этом случае Swift передаёт разработчику continuation — объект, у которого есть методы resume, позволяющие явно указать:
- успешный результат;
- ошибку.
Таким образом continuation — это мост между callback-миром и async/await.
Когда continuations действительно нужны
Continuations используются исключительно для адаптации не-async API:
- legacy сетевые клиенты;
- SDK сторонних библиотек;
- API с completion-handler’ами;
- делегаты (камера, геолокация, Bluetooth и т.д.).
Если API уже поддерживает async/await — continuations не нужны.
Виды continuations в Swift
Swift предоставляет четыре функции:
- withCheckedContinuation
- withCheckedThrowingContinuation
- withUnsafeContinuation
- withUnsafeThrowingContinuation
Различие основано на двух параметрах:
- Может ли операция завершиться с ошибкой
- Нужны ли runtime-проверки корректности
На практике:
- checked — безопасные, с проверками
- unsafe — без проверок, потенциально опасные
👉 В большинстве приложений следует использовать checked-версии.
withCheckedThrowingContinuation — основной рабочий инструмент
withCheckedThrowingContinuation используется, когда:
- адаптируемый API может вернуть ошибку;
- требуется безопасное поведение с проверками.
Сигнатура:
Swift гарантирует:
- resume будет вызван ровно один раз;
- continuation не будет потерян;
- при нарушении правил приложение упадёт с понятной ошибкой.
Базовый пример
Допустим, есть legacy-функция:
Адаптация в async/await:
Использование:
Ключевое правило continuations
Метод resume должен быть вызван ровно один раз.
Ошибки:
- resume не вызван → await зависает навсегда;
- resume вызван дважды → runtime-ошибка.
Checked-continuations выявляют такие ошибки сразу.
Важная проблема: отмена Task
Отмена задачи через:
не отменяет автоматически callback-based операцию, запущенную внутри continuation.
Это означает:
- операция может продолжать выполняться;
- ресурсы не освобождаются;
- callback может выполниться уже после отмены задачи.
Почему так происходит
Swift Concurrency:
- не управляет потоками напрямую;
- отмена — это лишь установка флага isCancelled.
Legacy API об этом флаге не знает.
Правильный подход: withTaskCancellationHandler
Для долгоживущих операций необходимо обрабатывать отмену вручную:
Что здесь важно
- onCancel — освобождение ресурсов
- проверка Task.isCancelled перед resume
- выбрасывание CancellationError
Checked vs Unsafe continuations
unsafe-continuations:
- не выполняют runtime-проверки;
- не предотвращают двойной resume;
- могут приводить к undefined behavior.
По рекомендациям команды Swift:
unsafe-continuations не должны использоваться в обычных приложениях без строгих причин и профилирования.
На практике checked-continuations почти всегда достаточны.
Практические рекомендации
- Используйте withCheckedThrowingContinuation по умолчанию
- Никогда не вызывайте resume более одного раза
- Не храните continuation вне замыкания
- Обрабатывайте отмену Task для долгих операций
- Не оптимизируйте преждевременно, выбирая unsafe
Заключение
Continuations — это ключевой инструмент для постепенной миграции к Swift Concurrency без переписывания всей кодовой базы.
Они позволяют:
- аккуратно оборачивать legacy API;
- писать линейный, читаемый async-код;
- контролировать ошибки и отмену задач.
При корректном использовании continuations делают код предсказуемым, безопасным и поддерживаемым — именно то, что ожидается от современного Swift-проекта.