Привет! После прочтения прошлых двух статей (первая, вторая) и вас есть все вводные - мы знаем, какие проблемы необходимо решить и знаем новую модель по работе с асинхронными задачами.

Теперь можно изучать новые синтаксические конструкции языка 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-разработчик


Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.

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