Асинхронне програмування у Dart: Future, async, await
Асинхронність є однією з фундаментальних умов для роботи з Dart. Навіть якщо дане поняття вам відоме з JavaScript, асинхронність у Dart має несуттєві відмінності, тож радимо прочитати цю статтю кожному.
Дана стаття є перекладом англомовного туторіалу з dart.dev.
Ця кодова лабораторія навчить вас писати асинхронний код за допомогою об'єкту Future та ключових слів async
, await
.
Щоб отримати максимальну віддачу від цієї кодової лабораторії, вам слід мати:
- Знання базового синтаксису Dart.
- Деякий досвід написання асинхронного коду іншою мовою.
Приблизний час для проходження цієї кодової лабораторії: 40-60 хвилин.
Чому асинхронний код важливий
Асинхронні операції дозволяють вашій програмі продовжувати працювати, одночасно чекаючи закінчення іншої операції. Ось декілька поширених асинхронних операцій:
- Отримання даних через мережу.
- Запис у базу даних.
- Зчитування даних з файлу.
Для виконання асинхронних операцій в Dart ви можете використовувати клас Future
та ключові слова async
і await
.
Приклад: Неправильне використання асинхронної функції
Наступний приклад показує неправильний спосіб використання асинхронної функції ( fetchUserOrder()
). Пізніше ви виправите приклад за допомогою async
та await
. Перш ніж запускати цей приклад, спробуйте виявити проблему — який, на вашу думку, буде результат?
Ось чому в прикладі не вдається вивести значення, яке fetchUserOrder()
повертає з запізненням у 2 секунди:
fetchUserOrder()
— це асинхронна функція, яка після затримки надає рядок, що описує замовлення користувача: “Large Latte”.- Щоб отримати замовлення користувача,
createOrderMessage()
слід викликатиfetchUserOrder()
і зачекати, поки замовлення буде повернуто. Через те, щоcreateOrderMessage()
виконує виведення до того, якfetchUserOrder()
завершує роботу,createOrderMessage()
не може отримати текстове значення, якеfetchUserOrder()
ще не повернуло. - Натомість
createOrderMessage()
отримує об'єкт, що позначає результат, який ще очікується: незавершену Future. Докладніше про Future ви дізнаєтесь у наступному розділі. - Оскільки
createOrderMessage()
не вдається отримати значення, що описує замовлення користувача, приклад виводить не “Large Latte”, а “Ваше замовлення:Instance of '_Future<String>'
”.
У наступних розділах ви дізнаєтеся про Future та про роботу з ними (за допомогою async
та await
), що дасть вам змогу написати код, який зможе коректно вивести "Large Latte" у консолі, який повертає функція fetchUserOrder()
.
Ключові терміни:
- Синхронна операція: синхронна операція блокує виконання інших операцій до свого завершення.
- Синхронна функція: синхронна функція виконує лише синхронні операції.
- Асинхронна операція: після запуску асинхронна операція дозволяє паралельно виконувати інші операції впродовж своєї роботи.
- Асинхронна функція: асинхронна функція виконує принаймні одну асинхронну операцію, а також може виконувати синхронні операції.
Що таке Future?
Future - це екземпляр класу Future. Future це представлення результату роботи асинхронної операції, що може мати два стани: незавершений або завершений.
Примітка: Незавершений (uncompleted) - термін у мові Dart, що стосується стану Future до того, як повертається значення.
Незавершений
Коли ви викликаєте асинхронну функцію, вона повертає незавершений Future. Цей Future чекає на успішне завершення асинхронної операції функції або на видачу помилки.
Завершений
Якщо асинхронна операція вдається, Future завершується (completes) значенням. В іншому випадку Future завершується помилкою.
Завершення зі значенням
Future<T>
завершується значенням типуT
. Наприклад, Future з типомFuture<String>
створює значення String (рядка). Якщо Future не повертає будь-якого значення, тоді це типFuture<void>
.
Завершення з помилкою
- Якщо асинхронна операція, виконана функцією, не вдається з будь-якої причини, Future завершується помилкою.
Приклад: вводимо Future у код
У цьому прикладі fetchUserOrder()
повертає Future, яке завершується вже після виводу в консолі. Оскільки функція не повертає яке-небудь корисне значення, fetchUserOrder()
має тип Future<void>
. Перш ніж запускати приклад, спробуйте спрогнозувати, що буде виведено першим: “Large Latte” або “Отримання замовлення користувача…”.
У попередньому прикладі, незважаючи на те, що fetchUserOrder()
виконується до виклику print()
на рядку 8, консоль відображає дані з рядка 8 (“Fetching user order…”) перед даними з fetchUserOrder()
(“Large Latte”). Це трапляється тому, що fetchUserOrder()
затримується перед тим, як надрукувати “Large Latte”.
Приклад: Завершення з помилкою
Запустіть цей приклад, щоб побачити, як Future завершується помилкою. Трохи пізніше ви дізнаєтесь, як правильно працювати з помилками у Future.
У цьому прикладі fetchUserOrder()
завершується помилкою, яка вказує на те, що ідентифікатор користувача недійсний.
Ви дізналися про Future та про те, як вони повертають значення, але як використати результати асинхронних операцій? У наступному розділі ви дізнаєтесь, як отримати результати за допомогою ключових слів async
та await
.
Короткий огляд
- Об'єкт типу
Future<T>
продукує значення типуT
- Якщо Future не продукує корисне значення, це тип
Future<void>
Future
має два стани: завершений та незавершений- Коли ви викликаєте функцію, що повертає Future, дана асинхронна задача стає у чергу та повертає значення через якийсь час, з затримкою
- Коли виконання Future завершується, результатом є значення заданого типу або помилка
Ключові терміни
- Future: клас Future у Dart
- a future (з малої літери): екземпляр класу Future, що позначає асинхронну задачу
Працюємо з Future: async та await
Ключові слова async
та await
створені для декларування асинхронних функцій та для використання їх результатів. Запам’ятайте ці дві основні рекомендації при використанні async
та await
:
- Щоб створити асинхронну функцію, додайте
async
перед тілом функції - Ключове слово
await
можна використовувати тільки в async функціях
Ось як перетворити синхронну функцію в асинхронну:
Спочатку додайте ключове слово async
перед тілом функції:
void main() async { ··· }
Якщо функція має оголошений тип, який вона повертає, змініть тип на Future<T>
, де T
є саме тим типом, що повертає функція. Якщо функція не повертає явного значення, тоді слід вказати тип Future<void>
:
Future<void> main() async { ··· }
Тепер, коли у вас є async
-функція, ви можете використовувати ключове слово await
, щоб дочекатися результату Future
:
print(await createOrderMessage());
Як показують наступні два приклади, ключові слова async
та await
продукують асинхронний код, який дуже схожий на синхронний код. Єдині відмінності виділено в асинхронному прикладі, який знаходиться праворуч від синхронного прикладу:
Асинхронний приклад має три відмінності:
- Тип, що повертає функція
createOrderMessage()
змінено зString
наFuture<String>
. - Ключове слово
async
додано перед тілом функціїcreateOrderMessage()
іmain()
. - Ключове слово
await
додано перед викликом асинхронних функційfetchUserOrder()
іcreateOrderMessage()
.
Ключові терміни
- async: Використовуйте ключове слово async, щоб зробити функцію асинхронною.
- async function:
async
-функція - це функція, позначена ключовим словомasync
. - await: Ви можете використовувати ключове слово
await
, щоб отримати завершений результат асинхронного виразу. Ключове словоawait
працює тільки в межахasync
-функцій.
Порядок виконання з async та await
Примітка: До версії Dart 2.0, async
-функція негайно повертала значення, не виконуючи код у тілі функції
Приклад: Виконання всередині async-функції
Запустіть цей приклад, щоб побачити, як працює виконання у тілі async
-функції. Як ви думаєте, яким буде результат?
Після запуску коду в попередньому прикладі спробуйте поміняти рядки 2 і 3 місцями:
var order = await fetchUserOrder(); print('Awaiting user order...');
Зверніть увагу як змінюється порядок виводу: тепер print('Awaiting user order')
з'являється після першого використання ключового слова await
в printOrderMessage()
.
Вправа: Попрактикуйтеся з async й await
Наступна вправа — це непрацюючий юніт-тест, який містить частково готові фрагменти коду. Ваше завдання — виконати вправу, написати такий код, щоб тест проходив успішно. Вам не потрібно реалізовувати функцію main()
.
Щоб імітувати асинхронні операції, використовуйте надані вам функції:
Future<String> fetchRole() {...} // Отримує короткий опис ролі користувача Future<int> fetchLoginAmount() {...} // Отримує кількість входів користувача в систему.
Частина 1: reportUserRole()
Доробіть функцію reportUserRole()
так, щоб вона робила наступне:
- Функція має повертати Future, яке завершується наступним рядком:
"User role: <user role>"
- Примітка: Ви повинні використовувати фактичне значення, яке повертає
fetchRole()
; копіювання та вставка значення з прикладу не дасть вам пройти тест - Приклад поверненого значення:
"User role: tester"
- Функція має отримувати роль користувача, викликаючи надану функцію
fetchRole()
.
Частина 2: reportLogins()
Реалізуйте async
-функцію reportLogins()
так, щоб вона робила наступне:
- Функція має повертати рядок
"Total number of logins: <# of logins>"
. - Примітка: Ви повинні використовувати фактичне значення, яке повертає
fetchLoginAmount()
; копіювання та вставка значення з прикладу не дасть вам пройти тест - Приклад поверненого значення з
reportLogins()
:"Total number of logins: 57"
- Функція має отримувати кількість входів, викликаючи надану функцію
fetchLoginAmount()
.
Зауважте: Якщо ваш код проходить усі тести, ви можете ігнорувати попередження з міткою info
Обробка помилок
Для обробки помилок у async
-функції, використовуйте конструкцію try-catch:
try { var order = await fetchUserOrder(); print('Awaiting user order...'); } catch (err) { print('Caught error: $err'); }
У межах async
-функції ви можете використовувати try-catch конструкції так само, як і в синхронному коді.
Приклад: async та await з використанням try-catch
Запустіть даний приклад, щоб побачити, як обробляти помилку в асинхронній функції. Як ви думаєте, яким буде результат?
Вправа: Практикуємося обробляти помилки
Наступна вправа надає практичні навички в асинхронному коді, використовуючи підхід, описаний у попередньому розділі. Для імітації асинхронних операцій вам надано цю функцію:
Future<String> fetchNewUsername() {...} /* Повертає нове ім’я користувача, яке можна використовувати для заміни старого. */
Використовуйте async
і await
для реалізації асинхронної функції changeUsername()
, що має робити наступне:
- Функція має викликати надану асинхронну функцію
fetchNewUsername()
і повертати її результат. - Приклад поверненого значення з
changeUsername()
:"jane_smith_92"
- Ловить (catch) будь-яку помилку, що виникає, і повертає значення рядка помилки.
- Ви можете використовувати метод toString() як для винятків (Exception), так і для помилок (Error)
Вправа: Складемо все докупи
Настав час потренуватися в тому, що ви дізналися, в одній заключній вправі. Для імітації асинхронних операцій ця вправа надає асинхронні функції fetchUsername()
та logoutUser()
:
Future<String> fetchUsername() {...}// Повертає ім'я поточного користувача Future<String> logoutUser() {...} /* Виконує вихід із поточного користувача та повертає ім’я користувача, що вийшов з системи */
Напишіть наступне:
Частина 1: addHello()
- Напишіть функцію,
addHello()
яка приймає один аргумент String. - Функція
addHello()
повертає свій аргумент типу String, перед яким стоїть "Hello".
Приклад:addHello('Jon')
повертає'Hello Jon'
.
Частина 2: greetUser()
- Напишіть функцію,
greetUser()
, що не приймає аргументів. - Щоб отримати ім’я користувача,
greetUser()
викликає надану асинхронну функціюfetchUsername()
. greetUser()
створює привітання для користувача, викликаючиaddHello()
, передаючи йому ім’я користувача та повертаючи результат.
Приклад: ЯкщоfetchUsername()
повертає'Jenny'
, тоgreetUser()
повертає'Hello Jenny'
.
Частина 3: sayGoodbye()
- Напишіть функцію
sayGoodbye()
, яка робить наступне: - Не приймає аргументів.
- Ловить будь-які помилки.
- Викликає надану асинхронну функцію
logoutUser()
. - Якщо
logoutUser()
не вдається,sayGoodbye()
повертає довільний рядок String. - У разі успіху
logoutUser()
,sayGoodbye()
повертає рядок'<result> Thanks, see you next time'
, де<result>
— значення рядка, яке повертається за допомогою викликуlogoutUser()
.
Що далі?
Вітаємо, ви закінчили роботу з кодовою лабораторією! Якщо ви хочете дізнатись більше, ось кілька підказок, куди йти далі:
- Експериментуйте у DartPad.
- Спробуйте іншу кодову лабораторію [англ.].
- Дізнайтеся більше про Future та асинхронність:
- Ознайомлення з Stream [англ.]: Дізнайтеся, як працювати з послідовністю асинхронних подій.
- Відео про Dart від Google [англ.]: перегляньте одне або кілька відео про асинхронне кодування. Або, якщо хочете, прочитайте статті, основані на цих відео. (Почніть із ізоляцій та циклів подій [англ.]).
- Завантажте Dart SDK.
Переклад виконано спеціально для @dart_itkpi.