异步编程: futures, async, await
这个codelab教我们如何使用future和关键字async
及await
来编写异步代码。使用内嵌的DartPad编辑器,可以通过运行示例代码和完成练习来测试所学的知识。
要通过codelab获取尽可能多的知识,需要具有如下:
- 基础Dart语法的知识。
- 一些在其它语言中编写异步代码的经验。
这个codelab涵盖如下内容:
- 如何及何时使用
async
和await
关键字。 - 如何使用
async
和await
影响执行顺序。 - 如何在
async
函数中使用try-catch
表达式处理异步调用的错误。
完成这一codelab的预估时间: 40-60分钟。
为什么异步代码很重要
异步操作让程序在等待另一个操作完成时完成任务。以下是一些常见的异步操作:
- 通过网络获取数据。
- 写入数据库。
- 从文件读取数据。
要在Dart中执行异步操作,可以使用 Future
类和 async
及 await
关键字。
示例:错误使用异步函数
以下示例显示使用一个异步函数(getUserOrder()
)的错误方式。稍后你可以使用async
和 await
来修复这一示例。在运行本例时,尝试定位问题,你认为输出会是什么?
以下是示例中打印getUserOrder()
最终产生值失败的原因:
getUserOrder()
是一个异步函数,在个延时后会提供描述用户订单的字符串:Large Latte。- 要获取用户的订单,
createOrderMessage()
应调用getUserOrder()
并等待其完成。因为createOrderMessage()
不会等待getUserOrder()
完成,createOrderMessage()
就无法获取到getUserOrder()
所最终产生的字符串值。 - 而
createOrderMessage()
获取一个等待任务完成的表示:一个未完成的 future.。在下一节中会学到有关future的更多内容。 - 因为
createOrderMessage()
无法获取到描述用户订单的值,示例中无法在控制台中打印Large Latte,而是打印了Your order is: Instance of ‘_Future’。
在下一节中会学到有关future, async
和 await
的知识,这样你就能够编写出让 getUserOrder()
在控制台打印出所期望值Large Latte所需的代码。
future是什么?
future (小写字母f)是一个 Future (大写字母 F)类的实例。 future代表异步操作的结果,并且可以有两种状态:已完成或未完成。
未完成
在调用异步函数时,它返回一个未完成的future。这个future等待函数的异步操作完成或抛出错误。
已完成
如果异步操作成功, future完成并生成值。否则它报错并完成。
完成并生成值
类型为Future<T>
的future完成并生成类型为T
的值。 例如,类型为 Future<String>
的future生成一个字符串值。如果 future不生成一个可用的值,那么 future的类型为Future<void>
。
报错并完成
如果由函数执行的异步操作会出于某种原因失败,future 会报错并完成。
示例: future简介
在下面的示例中, getUserOrder()
返回一个在打印到控制台后完成的future。因为它不返回可用的值,getUserOrder()
获取的类型为 Future<void>
。在运行示例前,请尝试预测哪个会先打印:Large Latte 或是 Fetching user order…。
在上例中,虽然在第8行的print()
调用前执行了getUserOrder()
,控制台中的输出中第8行的Fetching user order…在getUserOrder()
的输出之前出现 。这是因为在打印Large Latte之前getUserOrder()
存在延时。
示例:报错并完成
运行下例来查看future如何报错并完成。稍晚你会学到如何处理错误。
本例中, getUserOrder()
报错并完成,表明用户的 ID 是无效的。
你已经学到了 future及它们如何完成,但如何使用异步函数的结果呢?下一节中将学习如何通过async
和 await
关键字获取结果。
处理futures: async 和 await
关键字 async
和 await
提供一个声明方式来定义异步函数并使用它们的结果。在使用async
和 await
时记住这两个基本指南:
- 定义一个异步函数,在函数体之前添加
async
await
关键字仅能在async
函数中使用
以下是一个将main()
由同步转换为异步函数的示例。
首先,在函数体前添加关键字 async
:
1 |
main() async { |
如果函数声明了一个返回类型,那么更新该类型为 Future<T>
,其中 T
是函数返回值的类型。如果函数没有显式地返回值,那么返回类型为 Future<void>
:
1 |
Future<void> main() async { |
既然你有了一个 async
函数,可以使用 await
关键字来等待future完成:
1 |
print(await createOrderMessage()); |
如以下两个例子所示, async
和 await
关键字产生和同步代码非常相似的异步代码。不同之处在以异步代码中已高亮标出,如果屏幕够大的话,显示在同步示例的右侧。
示例:同步函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 同步 String createOrderMessage () { var order = getUserOrder(); return 'Your order is: $order'; } Future<String> getUserOrder() { // 想象一下这个函数 // 更加复杂、执行更慢 return Future.delayed( Duration(seconds: 4), () => 'Large Latte'); } // 同步 main() { print('Fetching user order...'); print(createOrderMessage()); } // 'Fetching user order...' // 'Your order is: Instance of _Future<String>' |
示例:异步函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 异步 Future<String> createOrderMessage () async { var order = await getUserOrder(); return 'Your order is: $order'; } Future<String> getUserOrder() { // 想象一下这个函数 // 更加复杂、执行更慢 return Future.delayed( Duration(seconds: 4), () => 'Large Latte'); } // 异步 main() async { print('Fetching user order...'); print(await createOrderMessage()); } // 'Fetching user order...' // 'Your order is: Large Latte' |
异步示例中有3处不同:
createOrderMessage()
的返回类型由String
变成了Future<String>
。async
关键字出现在createOrderMessage()
和main()
的函数体之前。await
关键字出现 在调用异步函数getUserOrder()
和createOrderMessage()
之前。
使用async 和 await的执行流程
async
函数会按同步运行,直至出现第一个await
关键字。 这表示在 async
函数体内,所有第一个await
之前的同步代码都会立即执行。
示例:在async函数内执行
运行如下示例来查看async
函数体中如何执行。你觉得输出会是什么呢?
在执行上例中的代码后,试着调换第4行和第5行:
1 2 |
var order = await getUserOrder(); print('Awaiting user order...'); |
注意输出的时机发生了变化,因为createOrderMessage()
中的 print('Awaiting user order')
出现在第一个 await
关键字之后。
练习:练习使用async 和 await
以下的练习是一个会失败的单元测试,包含部分完成的代码脚本。你的任务是通过编写代码来让测试通过以完成练习。无需实现 main()
。
要模拟异步操作,调用如下已为你提供的函数:
函数 | 类型签名 | 描述 |
---|---|---|
getRole() | Future<String> getRole() |
获取一个用户角色的短描述。 |
getLoginAmount() | Future<int> getLoginAmount() |
获取用户登录的次数。 |
Part 1: reportUserRole()
在 reportUserRole()
函数中添加代码完成如下操作:
- 返回一个完成及获取
"User role: <user role>"
字符串的future- 注意: 必须使用
getRole()
所返回的实际值;复制并粘贴示例返回值不会让测试通过。 - 示例返回值:
"User role: tester"
- 注意: 必须使用
- 通过调用所提供函数
getRole()
来获取用户角色。
Part 2: reportLogins()
实现一个 async
函数 reportLogins()
来执行如下操作:
- 返回字符串
"Total number of logins: <# of logins>"
。- 注意:必须使用
getLoginAmount()
所返回的实际值;复制并粘贴示例返回值不会让测试通过。 -
reportLogins()
的示例返回值:"Total number of logins: 57"
- 注意:必须使用
- 通过调用所提供函数
getLoginAmount()
来获取登录数。
处理错误
使用try-catch处理 async
函数中的错误:
1 2 3 4 5 6 |
try { var order = await getUserOrder(); print('Awaiting user order...'); } catch (err) { print('Caught error: $err'); } |
在 async
函数中,可以用同步代码中相同的方式编写 try-catch从句。
示例:带有try-catch的async 和 await
运行如下示例来查看如何处理异步函数中的错误。你觉得输出会是什么呢?
练习:练习处理错误
如下练习提供对处理异步代码错误的练习,使用前一节中所描述的方法。要模拟异步操作,你的代码将调用如下已为你提供的函数:
函数 | 类型签名 | 描述 |
---|---|---|
getNewUsername() | Future<String> getNewUsername() |
返回一个你可以用于替换旧名称的新用户名。 |
使用 async
和 await
来实现一个执行如下操作的异步函数 changeUsername()
:
- 调用所提供的异步函数
getNewUsername()
并返回其结果。changeUsername()
中的示例返回值是:"jane_smith_92"
- 捕获任意发生的错误并返回错误的字符串值。
- 可以使用 toString() 方法来字符串化 异常 和 错误。。
练习:对所有内容进行练习
是时候在最终练习中练习所有已学知识了。为模拟异步操作,本练习提供了异步函数getUsername()
和 logoutUser()
:
函数 | 类型签名 | 描述 |
---|---|---|
getUsername() | Future<String> getUsername() |
返回与当前用户关联的名称。 |
logoutUser() | Future<String> logoutUser() |
执行当前用户拿出并返回拿出的用户名。 |
编写如下内容:
Part 1: addHello()
- 编写接收单个字符串变量的函数
addHello()
。 addHello()
返回由文本Hello <string>所包围的字符串参数。
Part 2: greetUser()
- 编写一个不接收参数的函数
greetUser()
。 - 为获取用户名,
greetUser()
调用所提供的异步函数getUsername()
greetUser()
通过调用addHello()
为用户创建一个问候语, 向其传递用户名,并返回结果。- 例如,如果用户是Jenny,
greetUser()
应创建并返回如下内容:Hello Jenny
- 例如,如果用户是Jenny,
Part 3: sayGoodbye()
- 编写执行如下操作的函数
sayGoodbye()
:- 不接收参数。
- 捕获任意错误。
- 调用所提供的异步函数
logoutUser()
。
- 如果
logoutUser()
成功,sayGoodbye()
返回字符串<result> Thanks, see you next time,其中<result>是通过调用logoutUser()
返回的字符串值。
下一步进阶?
恭喜,你已完成了本codelab!如果想要学习更多,以下是一些探索更多课题的建议: