中级教程: 作用域函数(Scope Function)
在这一章中, 你在对扩展函数的理解的基础之上, 学习如何使用作用域函数来编写更加符合 Kotlin 惯用法的代码.
作用域函数(Scope Function)
在编程中, 作用域(scope) 是指一个能够识别变量或对象的区域. 最常见的作用域是全局作用域和局部作用域:
全局作用域(Global Scope) – 能够从程序任何位置访问的变量或对象.
局部作用域(Local Scope) – 只能在定义它的代码块或函数之内访问的变量或对象.
在 Kotlin 中, 还有作用域函数(Scope Function), 能够围绕一个对象创建临时作用域, 并执行一些代码.
作用域函数能够让你的代码更加简洁, 因为在临时作用域内, 你不必引用你的对象的名称. 根据作用域函数不同, 你可以通过关键字 this 来引用对象, 或者通过关键字 it, 将它作为一个参数来访问.
Kotlin 共有 5 个作用域函数: let, apply, run, also, 和 with.
每个作用域函数接受一个 Lambda 表达式参数, 并返回对象, 或返回 Lambda 表达式的结构. 在这篇教程, 我们会解释每个作用域函数, 以及如何使用.
let
当你想要在代码中执行 null 值检查, 然后对返回的对象执行进一步操作, 可以使用 let 作用域函数.
看看这个示例:
这个示例有 2 个函数:
sendNotification(), 有一个函数参数recipientAddress, 返回一个字符串.getNextAddress(), 没有函数参数, 返回一个字符串.
这个示例创建一个变量 address, 类型是可为 null 的 String. 但当你调用 sendNotification() 函数时这会成为问题, 因为这个函数要求 address 不能是 null 值. 结果是编译器会报告错误:
在初学者教程中, 你已经知道了可以使用 if 条件, 或使用 Elvis 操作符 ?:, 执行 null 值检查. 但如果你想要在之后的代码中使用返回的对象, 应该怎么办? 你可以使用 if 条件 和 一个 else 分支来实现:
但是, 更加简洁的方法是使用 let 作用域函数:
这个示例中:
创建一个变量, 名为
address.在
address变量上, 对let作用域函数使用一个安全调用.在
let作用域函数之内, 创建一个临时作用域.将
sendNotification()函数作为一个 Lambda 表达式, 传递给let作用域函数.使用临时作用域, 通过
it引用address变量.将结果赋值给
confirm变量.
通过这种方式, 你的代码能够处理 address 变量可能为 null 值的情况, 而且你能够在之后的代码中使用 confirm 变量.
apply
使用 apply 作用域函数, 能够在创建时而不是在之后的代码中初始化对象, 例如一个类实例. 这种方法能够让你的代码更加易于阅读和管理.
看看这个示例:
这个示例有一个 Client 类, 包含一个属性, 名为 token, 以及 3 个成员函数: connect(), authenticate(), 和 getData().
这个示例创建 Client 类的实例 client, 之后在 main() 函数中初始化它的 token 属性, 并调用它的成员函数.
尽管这个示例很小, 但在实际应用中, 在你创建一个类实例之后, 可能要经过一段时间才能配置和使用它(以及它的成员函数). 但是, 如果你使用 apply 作用域函数, 你就可以在代码的同一处, 创建, 配置, 并对你的类实例使用成员函数:
这个示例中:
创建
Client类的实例client.对
client实例使用apply作用域函数.在
apply作用域函数之内创建一个临时作用域, 因此在访问它的属性或函数时, 你不必明确的引用client实例.向
apply作用域函数传递一个 Lambda 表达式, 它更新token属性, 并调用connect()和authenticate()函数.在
main()函数中, 对client实例调用getData()成员函数.
你可以看到, 当你处理大段代码时, 这种方法会很方便.
run
与 apply 类似, 你可以使用 run 作用域函数来初始化一个对象, 但 run 最好的使用场景是, 在代码的某个特定时刻初始化一个对象, 并且 立即计算一个结果.
我们继续前面的 apply 函数示例, 但这一次你想要将 connect() 和 authenticate() 函数组合在一起, 使它们对每一个请求都会被调用.
例如:
这个示例中:
创建
Client类的实例client.对
client实例使用apply作用域函数.在
apply作用域函数之内创建一个临时作用域, 因此在访问它的属性或函数时, 你不必明确的引用client实例.向
apply作用域函数传递一个 Lambda 表达式, 它更新token属性.
main() 函数中:
创建一个
result变量, 类型为String.对
client实例使用run作用域函数.在
run作用域函数之内创建一个临时作用域, 因此在访问它的属性或函数时, 你不必明确的引用client实例.向
run作用域函数传递一个 Lambda 表达式, 它调用connect(),authenticate(), 和getData()函数.将结果赋值给
result变量.
现在你可以在后续代码中使用返回的结果了.
also
使用 also 作用域函数, 对一个对象完成一个额外的动作, 然后返回对象在代码中继续使用, 例如输出一个 log.
看看这个示例:
这个示例中:
创建
medals变量, 包含一个字符串 List.创建
reversedLongUpperCaseMedals变量, 类型为List<String>.对
medals变量使用.map()扩展函数.向
.map()函数传递一个 Lambda 表达式, 它通过it关键字引用medals, 并对它调用.uppercase()扩展函数.对
medals变量使用.filter()扩展函数.向
.filter()函数传递一个 Lambda 表达式, 作为判定条件, 它通过it关键字引用medals, 并检查medals变量中包含的字符串长度是否超过 4 个字符.对
medals变量使用.reversed()扩展函数.将结果赋值给
reversedLongUpperCaseMedals变量.打印输出
reversedLongUpperCaseMedals变量中包含的列表.
如果能在函数调用之间添加一些 log 会非常有用, 这样就可以看到 medals 变量发生了什么变化. also 函数能够帮助我们实现这一点:
现在, 在这个示例中:
对
medals变量使用also作用域函数.在
also作用域函数之内创建一个临时作用域, 因此在将它用作函数参数时, 你不必明确的引用medals变量.向
also作用域函数传递一个 Lambda 表达式, 它调用println()函数, 通过it关键字, 使用medals变量作为函数参数.
由于 also 函数返回对象, 它不仅能够用于 log 输出, 还适合于调试, 链接多个操作, 以及执行其它不影响代码主体流程的副作用操作.
with
与其它作用域函数不同, with 不是扩展函数, 因此语法不同. 你需要向 with 传递接受者对象作为参数.
当你想要对一个对象调用多个函数时, 可以使用 with 作用域函数.
看看这个示例:
这个示例创建一个 Canvas 类, 有 3 个成员函数: rect(), circ(), 和 text(). 每个成员函数打印输出由你提供的函数参数构建的一个句子.
这个示例创建 Canvas 类的实例 mainMonitorPrimaryBufferBackedCanvas, 然后对这个实例, 使用不同的函数参数调用一系列的成员函数.
你可以看到, 这段代码很难阅读. 如果你使用 with 函数, 代码会变得非常精简:
这个示例中:
使用
with作用域函数, 将mainMonitorSecondaryBufferBackedCanvas实例作为接受者对象.在
with作用域函数之内创建一个临时作用域, 因此在调用它的成员函数, 你不必明确的引用mainMonitorSecondaryBufferBackedCanvas实例.向
with作用域函数传递一个 Lambda 表达式, 使用不同的函数参数调用一系列的成员函数.
现在这段代码变得更加容易阅读了, 犯错误的可能性也更低了.
使用场景概述
本节介绍 Kotlin 中的各种作用域函数, 以及它们的主要使用场景, 目的是让你的代码更加符合 Kotlin 惯用法. 你可以将这个表作为一个快速参考. 需要注意的是, 要在你的代码中使用这些函数, 你并不需要完全理解它们如何工作.
函数 | 访问 | 返回值 | 使用场景 |
|---|---|---|---|
|
| Lambda 表达式的结果 | 在你的代码中执行 null 值检查, 然后对返回的对象执行后续操作. |
|
|
| 在创建时初始化对象. |
|
| Lambda 表达式的结果 | 在创建时初始化对象, 并 计算一个结果. |
|
|
| 在返回对象之前进行额外的操作 . |
|
| Lambda 表达式的结果 | 在一个对象上调用多个函数 . |
关于作用域函数, 详情请参见 作用域函数.
实际练习
习题 1
将 .getPriceInEuros() 函数重写为一个单一表达式函数, 它使用安全调用操作符 ?. 和 let 作用域函数.
- 提示
使用安全调用操作符
?.以便安全的访问getProductInfo()函数的priceInDollars属性. 然后, 使用let作用域函数, 将priceInDollars的值转换为欧元.
习题 2
你有一个 updateEmail() 函数, 它更新一个用户的 EMail 地址. 使用 apply 作用域函数来更新 EMail 地址, 然后使用 also 作用域函数打印输出一个 log 消息: Updating email for user with ID: ${it.id}.