Какая разница между async/await и completion handlers (closures) в Swift?

«Какая разница между async/await и completion handlers (closures) в Swift?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Completion handlers (замыкания) и async/await — это два способа работы с асинхронным кодом. async/await представляет собой синтаксический сахар над completion handlers, делающий асинхронный код линейным и читаемым.

Сравнение на примере сетевого запроса:

1. Подход с Completion Handler:

func fetchUserData(withId id: String, 
                   completion: @escaping (Result<User, Error>) -> Void) {
    let url = URL(string: "https://api.example.com/user/(id)")!

    URLSession.shared.dataTask(with: url) { data, response, error in
        // 1. Ручная проверка ошибок
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let data = data else {
            completion(.failure(NetworkError.noData))
            return
        }

        do {
            // 2. Декодирование в основном потоке (может блокировать UI)
            let user = try JSONDecoder().decode(User.self, from: data)
            DispatchQueue.main.async {
                // 3. Обязательный переход на main queue для UI-обновлений
                completion(.success(user))
            }
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// Использование (ведет к "Pyramid of Doom" или "Callback Hell"):
fetchUserData(withId: "123") { result in
    switch result {
    case .success(let user):
        self.updateUI(with: user)
        fetchUserAvatar(for: user) { avatarResult in // Вложенный асинхронный вызов
            // ...
        }
    case .failure(let error):
        self.showError(error)
    }
}

2. Подход с async/await (Swift 5.5+):

func fetchUserData(withId id: String) async throws -> User {
    let url = URL(string: "https://api.example.com/user/(id)")!

    // 1. `await` приостанавливает функцию, не блокируя поток
    let (data, _) = try await URLSession.shared.data(from: url)

    // 2. Декодирование. Функция `decode` также может быть async
    let user = try JSONDecoder().decode(User.self, from: data)

    // 3. Автоматический возврат в actor (например, MainActor) не требуется здесь,
    // но для UI-обновлений нужно вызвать `await MainActor.run`
    return user
}

// Использование (линейный код, похожий на синхронный):
Task { // Создает новый асинхронный контекст
    do {
        let user = try await fetchUserData(withId: "123")
        let avatar = try await fetchUserAvatar(for: user) // Последовательный вызов

        // Обновление UI должно происходить на главном потоке
        await MainActor.run {
            self.imageView.image = avatar
            self.nameLabel.text = user.name
        }
    } catch {
        await MainActor.run {
            self.showError(error)
        }
    }
}

Ключевые преимущества async/await:

  • Читаемость: Код выполняется сверху вниз, устраняя вложенность.
  • Упрощенная обработка ошибок: Используется знакомый try/catch вместо проверки Result в completion.
  • Безопасность потоков: Компилятор помогает избежать распространенных ошибок (например, вызов completion более одного раза).
  • Интеграция с акторами (Actors): Упрощает написание потокобезопасного кода.

Важно: async/await не заменяет полностью completion handlers. Они все еще нужны для взаимодействия с API, основанными на замыканиях, и для обратной совместимости.