← Назад к вопросам

Как создать свою обертку для методов с completion, используя async/await?

2.0 Middle🔥 251 комментариев
#Многопоточность и асинхронность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Создание обертки для методов с completion с помощью async/await

Стандартный подход для преобразования методов с completion-блоками в async/await заключается в использовании континуаций (continuations). Это позволяет "подвесить" (suspend) выполнение асинхронной функции до получения результата из старого API.

Основная концепция континуаций

Swift предоставляет механизм withCheckedContinuation и withCheckedThrowingContinuation для безопасного преобразования callback-based API в async/await. Континуация создает "точку возобновления" (resumption point) для асинхронного выполнения.

Пример базовой обертки

Рассмотрим пример обертки для метода, который возвращает результат через completion:

// Старый метод с completion
func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        completion(.success("Данные получены"))
    }
}

// Async обертка с использованием континуации
func fetchDataAsync() async throws -> String {
    return await withCheckedThrowingContinuation { continuation in
        fetchData { result in
            switch result {
            case .success(let value):
                continuation.resume(returning: value)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

Ключевые моменты реализации

  1. Выбор типа континуации:

    • withCheckedContinuation — для методов без ошибок
    • withCheckedThrowingContinuation — для методов, которые могут завершиться с ошибкой
  2. Обязательное возобновление: Континуация должна быть возобновлена (resumed) строго один раз. Невыполнение этого правила приведет к утечке памяти и непредсказуемому поведению.

  3. Обработка completion: Внутри completion-блока старого метода мы вызываем continuation.resume() с соответствующим результатом.

Более сложный пример с параметрами

// Старый метод с параметрами
func loadImage(url: URL, completion: @escaping (UIImage?) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        let image = UIImage(data: data ?? Data())
        completion(image)
    }.resume()
}

// Async обертка
func loadImageAsync(url: URL) async -> UIImage? {
    return await withCheckedContinuation { continuation in
        loadImage(url: url) { image in
            continuation.resume(returning: image)
        }
    }
}

Расширенный подход: создание универсальной обертки

Для масштабирования решения можно создать универсальную функцию-обертку:

func asyncWrapper<T>(
    for closure: @escaping (@escaping (T) -> Void) -> Void
) async -> T {
    return await withCheckedContinuation { continuation in
        closure { value in
            continuation.resume(returning: value)
        }
    }
}

// Использование
let result = await asyncWrapper(for: fetchData)

Особенности и рекомендации

  • Thread safety: Континуации безопасны относительно потоков, но важно обеспечить, что resume() вызывается из любого потока
  • Cancellation: При использовании async/await поддерживается механизм отмены задач (task cancellation), но для старого API нужно добавить собственную логику обработки отмены
  • Memory management: Убедитесь, что completion** не сохраняет континуацию** после вызова resume()
  • Performance: Континуации добавляют минимальные накладные расходы, но значительно улучшают читаемость кода

Практическое применение в проектах

При переходе с completion-based API на async/await рекомендуется:

  1. Создать обертки для критически важных API
  2. Протестировать обертки на граничных случаях (ошибки, таймауты)
  3. Использовать обертки постепенно, не нарушая существующую логику

Итог

Континуации предоставляют мощный и безопасный механизм для интеграции старого callback-based кода с современным async/await. Это позволяет постепенно модернизировать проект без полного переписывания существующего API, сохраняя инвестиции в ранее написанный код.