Используйте возможности продолжений при преобразовании обратных вызовов или API на основе делегирования.

Новый API параллелизма async/await Swift, выпущенный вместе со Swift 5.5, был наконец представлен на конференции WWDC 21 в июне прошлого года. Эта новая архитектура оправдала все ожидания, которые были у нас, разработчиков iOS, которые с нетерпением ждали этого с момента первого выпуска Swift в 2014 году и чьи надежды усилились в 2017 году благодаря Манифесту параллелизма Swift самого Криса Латтнера.

Из всех представленных новых API и инструментов новый синтаксис async/await, Актеры и Задачи привлекли наибольшее внимание сообщества Swift. И это правильно, эти новые дополнения значительно улучшили читаемость асинхронного кода и упростили обеспечение безопасности потоков в наших приложениях.

Однако тогда был представлен еще один механизм, который не привлек столько внимания, хотя он очень эффективен при работе с async/await: Continuations.

Что такое продолжения?

Согласно документации, Continuations — это механизм интерфейса между синхронным и асинхронным кодом, регистрирующий нарушения корректности.

Точнее, Продолжение — это объект, который фиксирует состояние программы в заданный момент, имея возможность продолжить его, когда это потребуется.

Это все еще может звучать очень абстрактно, но мы можем научиться ценить это глубже, рассматривая то, как Apple до сих пор реализует асинхронный код с обратными вызовами и шаблоном делегата.

Продолжения сияют во всей своей яркости при работе с этими двумя паттернами: они действуют как мост между старым миром (обратные вызовы и делегаты) и новым (async/await для достижения более читаемого и структурированного кода), помогая создавать новые фасады асинхронных функций. старые.

Это полезно при использовании сторонних библиотек или API Apple, содержащих эти шаблоны, как в случае, который мы увидим сегодня.

Фасад класса на основе делегата с продолжением

В нашей команде iOS нам нравится закрывать некоторые компоненты Apple или сторонних разработчиков собственными компонентами. Назначение этого паттерна многогранно, но нам особенно выделяются три момента:

  • Мы улучшаем читабельность и удобство использования этого компонента с помощью более простого API, который скрывает детали более сложного.
  • Мы упрощаем моки и, таким образом, улучшаем тестируемость нашего кода.
  • Более слабая связь позволяет легко заменить этот фасадный компонент, если мы захотим использовать новую технологию.

Возьмем, к примеру, наш класс LocationManager. Как указывалось ранее, мы хотим упростить и скрыть сложность Apple CLLocationManager, которая реализуется через шаблон делегирования через протокол CLLocationManagerDelegate.

Для простоты мы получаем местоположение пользователя в данный момент только один раз, без необходимости прослушивания изменений местоположения.

Несмотря на это, мы ничего не имеем против шаблона делегата как такового; мы рассматриваем его как еще один мощный инструмент в нашем наборе инструментов, который может быть очень полезен в нужном случае.

Возвращаясь к нашему случаю, важно отметить, что, поскольку нам нужно местоположение только один раз (выстрелил и забыл), мы можем преобразовать наш API в async/await, так же, как до того, как мы использовали промисы.

Если бы требовалось обновление при любом изменении местоположения пользователя, нам все равно пришлось бы использовать какую-то парадигму реактивного программирования, такую ​​​​как Combine или RxSwift, потому что существует непрерывный поток для наблюдения.

Эволюция быстрого параллелизма

История git файла LocationManager — очень интересный источник для отслеживания эволюции параллелизма в Swift и нашей команде.

После непродолжительного использования необработанного Delegate Apple API мы начали его облицовывать с помощью обратных вызовов, передавая новое местоположение клиенту через замыкание.

После этого мы открыли для себя силу Promises и создали собственную библиотеку Light Promises с открытым исходным кодом для их реализации, Pied Piper. (Опять же, если бы была необходимость в постоянном обновлении, мы бы использовали более реактивный подход).

Затем появился Combine, и мы заменили эту стороннюю библиотеку Promises аналогами Combine. И, наконец, настало время, якобы затянувшееся надолго, когда того вернувшегося Паблишера можно выгнать и заменить на ключевое слово await.

Фасад комбината

Как уже говорилось, последняя версия нашего LocationManager до async/await реализована с помощью издателей и промисов Combine:

Как вы можете видеть, с помощью свойства экземпляра обещания мы можем разрешить этому классу клиента, когда местоположение будет получено, сохраняя возвращенный издатель и реагируя при обновлении:

От объединения к асинхронному/ожиданию с продолжением

Этот подход достиг целей, которые мы ставили перед собой при создании фасада: он скрывает детали реализации Apple CoreLocation и предоставляет более простой API для работы.

Но в нашем ненасытном стремлении к улучшению мы поняли, что этот API можно сделать проще и читабельнее, преобразовав его в async/await. Как мы можем связать с ним шаблон асинхронного делегата CoreLocation? С помощью нашего нового друга Checked Continuation.

Аналогично тому, как мы возвращаем издателя, который впоследствии будет обновлен через свойство promise, теперь мы будем использовать проверенное продолжение, чтобы сохранить состояние программы при вызове updateLocation, чтобы продолжить работу после получения местоположения:

Мы тут:

  • Создайте typealias для нашего типа продолжения, чтобы сделать его более читаемым вместе с нашим классом.
  • Сохранить продолжение местоположения в свойстве этого типа
  • Когда местоположение запрашивается, мы запускаем продолжение, прося CLLocationManager обновить местоположение. Эта ссылка на продолжение хранится в нашем свойстве, поэтому ее можно возобновить с правильным значением, когда оно будет получено. (Или с ошибкой)
  • Таким образом, когда мы получаем обратный вызов делегата Core Location с обновленным местоположением, мы возобновляем наше продолжение с этим значением. Это возобновляет выполнение исходной функции. Если вместо этого мы получим ошибку, мы возобновим ее выброс.

Таким образом, код для вызова нашего менеджера намного проще:

Обратите внимание, насколько простым и понятным стал вызов для получения местоположения благодаря новому синтаксису async/await.

Важно отметить, что продолжение следует вызывать для возобновления только один раз.

Если мы не возобновим его, клиент функции будет ждать вечно, удерживая ресурсы на неопределенный срок.

Если мы сделаем это более одного раза в стиле Reactiveflow, ваше приложение рухнет. Здесь нет альтернатив.

Поэтому мы должны быть особенно осторожны, чтобы избежать этих бедствий. В нашем случае мы избегаем этого:

  • Возобновление продолжения, когда CoreLocation вернуло ошибку. Таким образом, мы уверены, что он будет возобновлен хотя бы один раз.
  • Установка продолжения на nil сразу после его возобновления. Таким образом, мы уверены, что он не будет вызываться более одного раза. Если местоположение потребуется снова, оно будет воссоздано.

Проверено или небезопасно?

Apple предлагает использовать два типа продолжений: проверенное и небезопасное. Как следует из их названия, из документации:

CheckedContinuation выполняет проверки во время выполнения на наличие отсутствующих или множественных операций возобновления. UnsafeContinuation избегает принудительного применения этих инвариантов во время выполнения, поскольку он призван стать механизмом с низкими издержками для взаимодействия задач Swift с циклами событий, методами делегирования, обратными вызовами и другими механизмами планирования, отличными от async. Однако во время разработки важна возможность проверки того, что инварианты поддерживаются при тестировании. Поскольку оба типа имеют одинаковый интерфейс, в большинстве случаев вы можете заменить один другим без внесения других изменений.

Поэтому, если вы хотите использовать небезопасные продолжения, вам необходимо:

  • Будьте уверены, что ваше Продолжение всегда будет возобновлено
  • Профилируйте свое приложение, чтобы убедиться, что дополнительные проверки, сопровождающие Checked Continuations, привели к некоторому снижению производительности.

Если какие-либо из этих пунктов неверны для вас, я предлагаю оставаться на безопасной территории, используя проверенные продолжения, учитывая стоимость отсутствия или нескольких операций возобновления.

Подведение итогов

В этой статье мы признали роль продолжений в новой модели параллелизма Swift и то, почему это может быть актуально для нас.

После краткого обзора важности фасадов для достижения чистого кода и истории параллелизма в нашем приложении мы приступили к переносу фасада основного местоположения с комбинированных обещаний на API с асинхронным синтаксисом с помощью продолжений.

Затем мы заявили о последствиях отказа от возобновления нашего продолжения или повторения его более одного раза, а также о том, когда следует выбирать небезопасные продолжения вместо проверенных.

Я надеюсь, что теперь у вас есть Continuations в качестве еще одного мощного инструмента в вашем наборе инструментов, готового к использованию при переходе от шаблонов обратного вызова или делегата к async/await. Если у вас есть вопросы, замечания или предложения, дайте мне знать.

Удачного кодирования!!