Привет! После прочтения прошлых двух статей (первая, вторая) и вас есть все вводные - мы знаем, какие проблемы необходимо решить и знаем новую модель по работе с асинхронными задачами.
Теперь можно изучать новые синтаксические конструкции языка Swift.
Вызов асинхронной функции
Обновленный синтаксис позволяет писать асинхронные функции похожими на синхронные. Это делает код более читабельным, также новые конструкции указывают компилятору в каких местах программа может остановиться (как мы и обсуждали ранее, такие места зовутся suspension points).
Асинхронные функции помечаются новым ключевым словом - async
.
Давайте сравним похожие по смыслу функции. Одна из них написана в старом стиле, с completion замыканием, а другая с использованием нового синтаксиса.
Задача:
Нам необходимо загрузить данные о пользователе по id
и после завершения загрузки выполнить какое-нибудь действие, например, лог в консоль с данными о пользователе.
struct UserInfo {}
// New Skool
func fetchUserInfo(for id: Int) async -> UserInfo
// Old Skool
func fetchUserInfo(for id: Int, completion: ((UserInfo) -> Void))
В новом синтаксисе у нас пропало замыкание и стало явно видно возвращаемый тип.
Как же вызвать данную функцию? Давайте попробуем вызвать эту функцию в рамках другой асинхронной функции - asyncContextFunc.
func asyncContextFunc() async {
fetchUserInfo(for: 101)
}
Компилятор не дает вызывать функцию и просит пометить ее вызов с помощью ключевого слова await
. Данное ключевое слово говорит системе и программисту, что здесь возможен разрыв выполнения тела функции и ее остановка.
В таких местах мы как бы говорим: “Окей, система, у тебя реально много задач, а вызов этой функции может занять время, поэтому я отдам тебе управление - ты решай, что для тебя приоритетнее”.
В зависимости от загруженности система может отложить выполнение текущей функции и заняться другими задачами, либо продолжить выполнение функции сразу.
Обновленный Runtime может сохранить весь стек вызова данной асинхронной задачи и перейти на выполнение других более приоритетных задач. В момент, когда управление данной функции вернется назад, поток может быть другим.
Сама Apple иллюстрирует работу await
ключевого слова так:
Пример
Давайте попробуем создать воссоздать пример с разработчиком, который пришел к нам на стажировку. У нас есть структура Developer
- это наш разработчик, у него есть метод повышения своего уровня.
Повышение идет так: Trainee -> Junior -> Middle -> Senior
struct Developer {
enum Grade: Int {
case trainee
case junior
case middle
case senior
}
let name: String
var grade: Grade
// Вызов данной функции занимает очень длительное время
mutating func upGrade() async { }
}
Метод upGrade
занимает длительное время и по итогу повышает уровень нашего разработчика - поэтому функция mutating
, т.к. изменяет нашу структуру. Сам метод не выполняет больших вычислений, его задача просто ждать, пока разработчик сам повысит свои знания.
await developer.upGrade() // Junior
await developer.upGrade() // Middle
await developer.upGrade() // Senior
Таким образом, мы можем выполнить постепенное повышение grade
нашего разработчика использования замыканий. Заметим, что мы не блокируем поток - он продолжит выполнение, как разработчик повысит свой грейд. В старом синтаксисе это было бы так:
mutating func upGrade(completion: (() -> Void)?) { }
developer.upGrade() { // Junior
developer.upGrade() { // Middle
developer.upGrade(completion: nil) // Senior
}
}
Обработка ошибок
Обработку ошибок и structured concurrency
мы разберем в отдельной статье, сейчас лишь обсудим синтаксис и ее вид.
В Swift и до этого была возможность обрабатывать выбрасываемые функциями ошибки, характерная для большого количества языков. Для этих целей в системе зарегистрированы следующие ключевые слова: throws
, try
, catch
. Однако раньше они были применимы по большей части к синхронным функциям. Сейчас же эти конструкции можно использовать и в написании асинхронного кода. Перепишем пример с загрузкой данных с учетом обработки ошибки:
func fetchUserInfo(for id: Int) async throws -> UserInfo
func fetchUserInfo(for id: Int, completion: ((Result<UserInfo, Error>) -> Void))
Теперь при вызове нашей функции необходимо обрабатывать возможные ошибки с помощью конструкций языка. Если же этого мы не сделаем и вызовем функцию просто так, то получим:
// Call can throw, but it is not marked with 'try' and the error is not handled
let userInfo = await fetchUserInfo(for: 101)
Это устранит огромное количество проблем, которые могли быть вызваны тем, что в определённом месте программист мог забыть вызвать callback. (Мы рассматривали эту проблему в первой статье - “Many mistakes are easy to make”). Такие места сложно было найти при bugfix-е, а теперь их сам подсказывает компилятор!
Типы асинхронных функций
В proposal у Apple есть интересный пример, на котором явно видно, каким образом можно привести одну функцию к типу другой:
struct FunctionTypes {
var syncNonThrowing: () -> Void
var syncThrowing: () throws -> Void
var asyncNonThrowing: () async -> Void
var asyncThrowing: () async throws -> Void
mutating func demonstrateConversions() {
// Okay to add 'async' and/or 'throws'
asyncNonThrowing = syncNonThrowing
asyncThrowing = syncThrowing
syncThrowing = syncNonThrowing
asyncThrowing = asyncNonThrowing
// Error to remove 'async' or 'throws'
syncNonThrowing = asyncNonThrowing // error
syncThrowing = asyncThrowing // error
syncNonThrowing = syncThrowing // error
asyncNonThrowing = syncThrowing // error
}
}
Синхронная функция может “встать” на место асинхронной, но не наоборот. Функция, которая не выбрасывает ошибки, может “встать” на место функции, которая выбрасывает ошибки.
Task
Давайте попробуем вызвать любую async
функцию в методе viewDidLoad
. Компилятор ругнется и скажет:
Он говорит, что асинхронная функция может быть вызвана только в рамках асинхронного контекста. Код, написанный в теле асинхронной функции, уже находится в рамках асинхронного контекста, но что делать, если такой код необходимо вызывать в синхронном контексте, например, viewDidLoad
?
Для это необходимо создать сущность Task, которая предоставляет такой контекст и вызвать ее там.
func asyncFunction() async { }
override func viewDidLoad() {
Task {
await asyncFunction()
}
}
Task - это базовая единица concurrency в системе. Каждая асинхронная функция выполняется в рамках задачи. Здесь Apple проводит аналогию с Thread
: Task
для асинхронной функции то же самое, что Thread
для синхронной. Если вы не забыли модель корутин, то вот ее имплементация в Swift.
Более подробно данную тему мы будем разбирать в следующей статье “Structured Concurrency #1”.
Вывод
Мы познакомились с основами абсолютно новым для iOS-разработки синтаксисом языка Swift, его преимуществами над работой с замыканиями и потоками, увидели обработку ошибок. Самое интересное, что это только верхушка айсберга - дальше больше!
Никита Сосюк
iOS-разработчик
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.