Что такое 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, включая производительность, память и сопровождение.