中级教程: 作用域函数(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}
.