异步编程:futures、async、await。
— 焉知非鱼这个 codelab 教你如何使用 futures
、async
和 await
关键字编写异步代码。使用内嵌的 DartPad 编辑器,你可以通过运行示例代码和完成练习来测试你的知识。
这个 codelab 教你如何使用 futures
、async
和 await
关键字编写异步代码。使用内嵌的 DartPad 编辑器,你可以通过运行示例代码和完成练习来测试你的知识。
要想从这个 codelab 中获得最大的收获,你应该具备以下条件。
- 掌握基本的 Dart 语法
- 有用其他语言编写异步代码的经验。
这个 codelab 包括以下材料。
- 如何以及何时使用
async
和await
关键字。 - 使用
async
和await
如何影响执行顺序。 - 如何在
async
函数中使用try-catch
表达式处理异步调用中的错误。
估计完成这个代码实验的时间。40-60分钟
注意:本页面使用嵌入式 DartPads 来显示示例和练习。如果你看到的是空框而不是 DartPads,请转到 DartPad 故障排除页面。
为什么异步代码很重要 #
异步操作让你的程序在等待另一个操作完成时完成工作。下面是一些常见的异步操作。
- 通过网络获取数据。
- 写入数据库。
- 从文件中读取数据。
要在 Dart 中执行异步操作,你可以使用 Future
类以及 async
和 await
关键字。
例子: 错误地使用异步函数 #
下面的例子显示了使用异步函数(fetchUserOrder()
)的错误方法。稍后你将使用 async
和 await
来修复这个例子。在运行这个例子之前,试着发现这个问题-你认为输出会是什么?
// This example shows how *not* to write asynchronous Dart code.
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is more complex and slow.
Future.delayed(
Duration(seconds: 2),
() => 'Large Latte',
);
void main() {
print(createOrderMessage());
}
下面是这个例子为什么不能打印 fetchUserOrder()
最终产生的值。
fetchUserOrder()
是一个异步函数,在延迟之后,提供一个描述用户订单的字符串:“Large Latte”。- 为了得到用户的订单,
createOrderMessage()
应该调用fetchUserOrder()
,并等待其完成。由于createOrderMessage()
没有等待fetchUserOrder()
完成,createOrderMessage()
无法获得fetchUserOrder()
最终提供的字符串值。 - 取而代之的是,
createOrderMessage()
得到的是待完成工作的表示:一个未完成的未来。您将在下一节了解更多关于未来的信息。 - 因为
createOrderMessage()
没有得到描述用户订单的值,所以这个例子没有打印 “Large Latte” 到控制台,而是打印 “Your order is: Instance of ‘_Future’"。
在接下来的章节中,你将学习关于 futures 和关于使用 futures 的工作(使用 async
和 await
),这样你就能编写必要的代码,使 fetchUserOrder()
向控制台打印所需的值(“Large Latte”)。
关键术语:
- 同步操作: 同步操作会阻止其他操作的执行,直到它完成。
- 同步函数:同步函数只执行同步操作。
- 异步操作:异步操作一旦启动,就允许其他操作在它完成之前执行。
- 异步函数:异步函数至少执行一个异步操作,也可以执行同步操作。
什么是未来? #
future(小写 “f”)是 Future(大写 “F”)类的一个实例。一个 future 代表异步操作的结果,可以有两种状态:未完成或完成。
注意:未完成是一个 Dart 术语,指的是一个未来的状态,在它产生一个值之前。
未完成的 #
当你调用一个异步函数时,它会返回一个未完成的未来。这个未来正在等待函数的异步操作完成或抛出一个错误。
已完成的 #
如果异步操作成功,未来就以一个值完成。否则它将以一个错误完成。
用一个值来完成 #
类型为 Future<T>
的 future 用一个类型为 T
的值来完成。例如,一个类型为 Future<String>
的 future 会产生一个字符串值。如果一个 future 没有产生一个可用的值,那么 future 的类型是 Future<void>
。
用一个错误来完成 #
如果函数执行的异步操作因为任何原因而失败,future 就会以错误的方式完成。
例子: 介绍 future #
在下面的例子中,fetchUserOrder()
返回一个在打印到控制台后完成的 future。因为它没有返回一个可用的值,fetchUserOrder()
的类型是 Future<void>
。在运行这个例子之前,试着预测一下哪个会先打印:“Large Latte” 或 “Fetching user order…"。
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info from another service or database.
return Future.delayed(Duration(seconds: 2), () => print('Large Latte'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
在前面的例子中,尽管 fetchUserOrder()
在第8行的 print()
调用之前执行,控制台还是在 fetchUserOrder()
的输出 (“Large Latte”) 之前显示了第8行的输出 (“Fetching user order…")。这是因为 fetchUserOrder()
在打印 “Large Latte” 之前会有延迟。
例子: 完成时出现错误 #
运行下面的例子,看看未来如何完成一个错误。稍后你将学习如何处理错误。
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info but encounters a bug
return Future.delayed(Duration(seconds: 2),
() => throw Exception('Logout failed: user ID is invalid'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
在这个例子中,fetchUserOrder()
完成时出现错误,表明用户ID无效。
你已经学习了 future 和它们如何完成,但你如何使用异步函数的结果呢?在下一节中,你将学习如何使用 async
和 await
关键字来获取结果。
快速回顾:
- 一个
Future<T>
实例会产生一个T
类型的值。 - 如果一个 future 没有产生一个可用的值,那么 future 的类型是
Future<void>
。 - 一个 future 可以处于两种状态之一:未完成或完成。
- 当你调用一个返回 future 的函数时,函数会把要做的工作排队,并返回一个未完成的 future。
- 当一个 future 的操作完成时,future 以一个值或以一个错误完成。
关键术语:
- Future: Dart Future 类。
- future:Dart
Future
类的一个实例。
使用 future:async 和 await #
async
和 await
关键字提供了一种声明式的方式来定义异步函数并使用它们的结果。在使用 async
和 await
时,请记住以下两个基本准则。
- 要定义一个异步函数,请在函数主体前添加
async
。 await
关键字只能在async
函数中使用。
下面是一个将 main()
从同步函数转换为异步函数的例子。
首先,在函数体前添加 async
关键字:
void main() async { ··· }
如果函数有声明的返回类型,那么更新类型为 Future<T>
,其中 T 是函数返回的值的类型。如果函数没有明确返回值,那么返回类型为 Future<void>
。
Future<void> main() async { ··· }
现在你已经有了一个 async
函数,你可以使用 await
关键字来等待一个 future 的完成:
print(await createOrderMessage());
正如下面两个例子所显示的,async
和a wait
关键字导致异步代码看起来很像同步代码。唯一的区别在异步示例中突出显示,如果你的窗口足够宽,它就在同步示例的右边。
示例:同步函数
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is
// more complex and slow.
Future.delayed(
Duration(seconds: 2),
() => 'Large Latte',
);
void main() {
print('Fetching user order...');
print(createOrderMessage());
}
Fetching user order...
Your order is: Instance of _Future<String>
例子:异步函数
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is
// more complex and slow.
Future.delayed(
Duration(seconds: 2),
() => 'Large Latte',
);
Future<void> main() async {
print('Fetching user order...');
print(await createOrderMessage());
}
Fetching user order...
Your order is: Large Latte
异步示例在三个方面有所不同。
createOrderMessage()
的返回类型从String
变为Future<String>
。async
关键字出现在createOrderMessage()
和main()
的函数体之前。await
关键字出现在调用异步函数fetchUserOrder()
和createOrderMessage()
之前。
关键术语:
- async: 你可以在一个函数的主体前使用
async
关键字来标记它为异步函数。 - async 函数:
async
函数是一个标有async
关键字的函数。 - await:可以使用
await
关键字来获取异步表达式的完成结果。await
关键字只在async
函数中起作用。
使用 async 和 await 的执行流程 #
一个异步函数在第一个 await
关键字之前是同步运行的。这意味着在一个 async
函数体中,第一个 await
关键字之前的所有同步代码都会立即执行。
版本说明:在 Dart 2.0 之前,一个异步函数立即返回,而不会在异步函数体中执行任何代码。
例子:在异步函数内执行。在异步函数中执行 #
运行下面的例子,看看如何在异步函数体中执行。你认为输出会是什么?
Future<void> printOrderMessage() async {
print('Awaiting user order...');
var order = await fetchUserOrder();
print('Your order is: $order');
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex and slow.
return Future.delayed(Duration(seconds: 4), () => 'Large Latte');
}
Future<void> main() async {
countSeconds(4);
await printOrderMessage();
}
// You can ignore this function - it's here to visualize delay time in this example.
void countSeconds(int s) {
for (var i = 1; i <= s; i++) {
Future.delayed(Duration(seconds: i), () => print(i));
}
}
运行上例中的代码后,尝试将第2行和第3行反过来。
var order = await fetchUserOrder();
print('Awaiting user order...');
注意到输出的时间发生了变化,现在 print('Awaiting user order')
出现在 printOrderMessage()
中第一个 await
关键字之后。
练习: 练习使用 async 和 await #
下面的练习是一个失败的单元测试,其中包含部分完成的代码片段。你的任务是通过编写代码使测试通过来完成练习。你不需要实现 main()
。
为了模拟异步操作,调用以下函数,这些函数是为你提供的。
函数 | 类型签名 | 描述 |
---|---|---|
fetchRole() | Future fetchRole() | 获取用户角色的简短描述。 |
fetchLoginAmount() | Future fetchLoginAmount() | 获取用户的登录次数。 |
第1部分:reportUserRole()
为 reportUserRole()
函数添加代码,使其执行以下操作。
- 返回一个以下列字符串完成的 future:
"User role: <user role>"
。- 注意:你必须使用
fetchRole()
返回的实际值;复制和粘贴示例返回值不会使测试通过。 - 示例返回值: “User role: tester”
- 注意:你必须使用
- 通过调用提供的函数
fetchRole()
获取用户角色。
第二部分:reportLogins()
实现一个异步函数 reportLogins()
,使其执行以下操作。
- 返回字符串 “Total number of logins: <# of logins>"。
- 注意:你必须使用
fetchLoginAmount()
返回的实际值;复制和粘贴示例返回值不会使测试通过。 reportLogins()
的返回值示例:"Total number of logins: 57"
。
- 注意:你必须使用
- 通过调用提供的函数
fetchLoginAmount()
来获取登录次数。
Future<String> reportUserRole() async {
var username = await fetchRole();
return 'User role: $username';
}
Future<String> reportLogins() async {
var logins = await fetchLoginAmount();
return 'Total number of logins: $logins';
}
注意:如果你的代码通过了测试,你可以忽略信息级的消息。
处理错误 #
要处理 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<void> printOrderMessage() async {
try {
var order = await fetchUserOrder();
print('Awaiting user order...');
print(order);
} catch (err) {
print('Caught error: $err');
}
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex.
var str = Future.delayed(
Duration(seconds: 4),
() => throw 'Cannot locate user order');
return str;
}
Future<void> main() async {
await printOrderMessage();
}
练习: 练习处理错误 #
下面的练习提供了使用异步代码处理错误的练习,使用上一节中描述的方法。为了模拟异步操作,你的代码将调用以下函数,该函数为你提供。
| 函数 | 类型签名 | 描述 | | fetchNewUsername() | Future fetchNewUsername() | 返回你可以用来替换旧用户名的新用户名。|
使用 async
和 await
来实现一个异步的 changeUsername()
函数,该函数执行以下操作。
- 调用提供的异步函数
fetchNewUsername()
并返回其结果。changeUsername()
的返回值示例: “jane_smith_92”
- 捕获任何发生的错误并返回错误的字符串值。
- 你可以使用 toString() 方法对 Exceptions 和Errors 进行字符串化。
Future<String> changeUsername () async {
try {
return await fetchNewUsername();
} catch (err) {
return err.toString();
}
}
练习: 把所有的东西放在一起 #
现在是时候在最后一个练习中练习所学的知识了。为了模拟异步操作,本练习提供了异步函数 fetchUsername()
和 logoutUser()
:
| 函数 | 类型签名 | 描述 | | fetchUsername() | Future fetchUsername() | 返回与当前用户相关联的名称。 | | logoutUser() | Future logoutUser() | 执行当前用户的注销,并返回被注销的用户名。 |
编写以下内容。
第一部分:addHello()
- 编写一个函数
addHello()
,它接受一个单一的String
参数。 addHello()
返回它的String
参数,前面加 ‘Hello’。 例如:addHello('Jon')
返回 ‘Hello Jon’。
第二部分:greetUser()
- 编写一个不接受参数的函数
greetUser()
。 - 为了得到用户名,
greetUser()
调用提供的异步函数fetchUsername()
。 greetUser()
通过调用addHello()
为用户创建一个问候语,传递用户名,并返回结果。 例子: 如果fetchUsername()
返回 ‘Jenny’, 那么greetUser()
返回 ‘Hello Jenny’.
第三部分:sayGoodbye()
-
编写一个函数
sayGoodbye()
,它的功能如下。- 不接受任何参数
- 捕获任何错误。
- 调用所提供的异步函数 logoutUser().
-
如果
logoutUser()
失败,sayGoodbye()
返回任何你喜欢的字符串。 -
如果
logoutUser()
成功,sayGoodbye()
返回字符串'<result> Thanks, see you next time'
,其中<result>
是调用logoutUser()
返回的字符串值。
String addHello(user) => 'Hello $user';
Future<String> greetUser() async {
var username = await fetchUsername();
return addHello(username);
}
Future<String> sayGoodbye() async {
try {
var result = await logoutUser();
return '$result Thanks, see you next time';
} catch (e) {
return 'Failed to logout user: $e';
}
}
下一步是什么? #
恭喜你,你已经完成了 codelab 的学习!如果你还想了解更多,这里有一些下一步的建议。
- 玩玩 DartPad。
- 尝试另一个 codelab。
- 学习更多关于 futures 和异步的知识。
- Streams tutorial: 学习如何使用异步事件的序列。
- 来自 Google 的 Dart视频: 观看一个或多个关于异步编码的视频。或者,如果你喜欢,阅读基于这些视频的文章。(从隔离和事件循环开始。)
- 获取 Dart SDK。
如果你对使用嵌入式 DartPads 感兴趣,就像这个 codelab 一样,请看教程中使用 DartPad 的最佳实践。