SwiftMiddleTechnical

Что такое async/await в Swift и чем он отличается от completion handlers?

async/await в Swift — языковая поддержка структурированного параллелизма (Swift 5.5+). Функция помечается async, вызывается через await; компилятор гарантирует обработку ошибок и отмену через Task. Completion handlers не дают таких гарантий.

async/await — языковые ключевые слова, введённые в Swift 5.5 (iOS 15+), которые позволяют писать асинхронный код в линейном стиле. Компилятор сам разбивает функцию на «продолжения» (continuations), которые возобновляются в нужный момент без явных callback-ов.

Как это работает

Функция, помеченная async, может быть приостановлена в точках await без блокировки потока. Исполнение передаётся другим задачам. Когда результат готов, задача возобновляется — возможно на том же, возможно на другом потоке пула.

import Foundation

// Модель данных
struct User: Decodable {
    let id: Int
    let name: String
}

// Сервис — async throws функция
func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(User.self, from: data)
}

// Вызов из UI-слоя (SwiftUI)
// @MainActor гарантирует обновление UI на главном потоке
@MainActor
func loadUser() async {
    do {
        let user = try await fetchUser(id: 1)
        print("Loaded: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

// Эквивалент на completion handlers (старый стиль)
func fetchUserLegacy(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)")!
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(URLError(.badServerResponse)))
            return  // если забыть return — completion вызовется дважды!
        }
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// Параллельный запрос двух пользователей
func fetchTwoUsers() async throws -> (User, User) {
    async let first = fetchUser(id: 1)
    async let second = fetchUser(id: 2)
    return try await (first, second)  // оба запроса идут параллельно
}

// Отмена через Task
func startLoad() -> Task<Void, Never> {
    Task { @MainActor in
        guard !Task.isCancelled else { return }
        try? await loadUser()
    }
}
// task.cancel() — кооперативная отмена

Ключевые отличия от completion handlers

Компилятор отслеживает ошибки: async throws заставляет вызывающий код обработать ошибку; в completion handler можно случайно проигнорировать `error`.
Гарантия однократного вызова: suspension point вызывается ровно один раз, callback можно забыть вызвать или вызвать дважды.
Structured Concurrency: дочерние задачи автоматически отменяются вместе с родительской; `Task.cancel()` в completion handlers нет вообще.
Читаемость: цепочка из 3 последовательных запросов — 3 строки с `await`; в completion handlers — «адская пирамида» вложенных closures.

Подводные камни

  • Неявный поток выполнения. После await код может продолжиться на другом потоке. Обновлять UI без @MainActor — гонка данных. Всегда аннотируйте View-модели @MainActor или явно переключайтесь через await MainActor.run { }.
  • Кооперативная отмена не автоматическая. Task.cancel() устанавливает флаг, но ваш код должен проверять Task.isCancelled или использовать try Task.checkCancellation(). Долгие CPU-циклы без этой проверки продолжат работу даже после отмены.
  • Retain cycle в замыканиях-продолжениях. withCheckedContinuation и withCheckedThrowingContinuation нужно возобновлять ровно один раз. Забытый вызов continuation.resume приводит к утечке задачи и зависанию.
  • Смешивание async и delegate/notification. Делегаты UIKit вызываются синхронно, но ваш async-обработчик не блокирует их поток. Используйте AsyncStream или AsyncChannel из swift-async-algorithms для bridging.
  • Использование Task { } внутри init. Задача запускается вне structured context и живёт дольше объекта, если не сохранить хендл и не вызвать cancel() в deinit. Особенно опасно в SwiftUI View-моделях.
  • Блокировка main actor чрезмерным serial кодом. Если пометить весь класс @MainActor и делать в нём тяжёлые вычисления, UI будет тормозить. Выносите CPU-работу в Task.detached или await Task.detached { }.value.
  • Ошибка «expression is async but is not marked with await». Выпадает при вызове async-функции из синхронного контекста. Нельзя просто «добавить await» — нужно заворачивать в Task { } или менять сигнатуру caller-а.
  • Перебор с actor-изоляцией. Каждый переход между actor-ами — потенциальный suspension point. Частые мелкие вызовы await actor.property в цикле дают overhead на переключение контекста. Группируйте чтения в одном batch-методе.

Common mistakes

  • Сводить «async/await в Swift и чем он отличается от completion handlers» к синтаксису и не объяснять система типов.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-15.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «async/await в Swift и чем он отличается от completion handlers» и подтверждает ее корректным примером.
  • Умеет связать ответ с ARC, тестированием и отладкой на устройстве.
  • Называет ограничения подхода swift-15, включая производительность, память и сопровождение.

Sources

Related topics

Что такое `async`/`await` в Swift и чем он отличается от completion handlers? | Talanto