作用域函数在 Kotlin 中非常有用,可以帮助我们管理代码并编写清晰易读的代码。
什么是作用域函数?
Kotlin 标准库中包含几个函数,其唯一目的是在对象的上下文中执行一段代码块。当我们在对象上调用这样的函数并提供一个 lambda 表达式时,它形成了一个临时作用域。在这个作用域中,我们可以通过对象的属性和函数来访问该对象,而无需使用对象的名称。这些函数被称为作用域函数。Kotlin 中共有五个作用域函数:let
、run
、with
、apply
和 also
。
关于 this
和 it
-
this
:在run
、with
和apply
函数中,我们可以使用 lambda 接收者关键字this
来引用上下文对象。因此,在它们的 lambda 表达式中,可以像在普通类函数中一样访问对象。在大多数情况下,当访问接收者对象的成员时,我们可以省略this
,从而使代码更简洁。然而,如果省略了this
,很难区分接收者成员和外部对象或函数之间的区别。因此,在主要通过调用其函数或为属性赋值来操作对象成员的 lambda 中,建议将上下文对象作为接收者 (this
)。
val adam = Person("Adam").apply {
age = 20 // 与 this.age = 20 相同
city = "London"
}
println(adam)
-
it
:let
和also
函数将上下文对象作为 lambda 参数引用。如果未指定参数名称,则可以使用隐式的默认名称it
来访问对象。使用it
比使用this
更简洁,使用it
的表达式通常更易读。然而,当调用对象的函数或属性时,不能像使用this
那样隐式地访问对象。因此,当对象主要作为函数调用的参数时,通过it
访问上下文对象更好。如果在代码块中使用多个变量,则使用it
也更好。
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() 生成的值为 $it")
}
}
val i = getRandomInt()
println(i)
使用作用域函数的应用场景
作用域函数可以使代码更加清晰、易读和简洁,这是 Kotlin 语言的主要特点之一。
作用域函数的类型有五种:let、run、with、apply、also
这些函数之间的主要区别有两点:
- 引用上下文对象的方式(使用
this
或it
关键字) - 返回值(返回上下文对象或 lambda 结果)
T.() 是让lambda表达式里面持有了this(run函数), (T) 是让lambda表达式里面持有了it(let函数)
Lambda表达式的特点是,最后一行会自动被认为是返回值类型,
作用域函数比较表:
函数 | 上下文对象引用 | 返回值 |
let | it | lambda 结果 |
run | this | lambda 结果 |
with | this | lambda 结果 |
apply | this | 对象本身 |
also | it | 对象本身 |
let
函数
- 上下文对象:作为参数(
it
) - 返回值:lambda 结果
使用场景:let
函数经常用于处理可空对象以避免空指针异常。可以使用安全调用操作符(?.
)结合 let
来进行空安全调用。它仅在非空值时执行代码块。
-
可以用于在调用链中的结果上调用一个或多个函数。
示例:
// 链式调用
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
// 使用 let
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }
.filter { it > 3 }
.let { println(it) } // 可以继续添加更多函数调用
-
空变量检查
var str: String? = null
// processNonNullString(str) // 编译错误:str 可能为空
var length = str?.let {
println("let() 在 $it 上调用")
processNonNullString(it) // OK:'it' 在 '?.let { }' 内部不为空
it.length
}
let源码分析:
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
- inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
- <T, R> T.let : T代表是要为T而扩展出一个函数名let(任何类型都可以 万能类型.let), R代表是Lambda表达式最后一行返回的类型
- block: (T) -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最一行返回推断的类型
- : R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个let函数的返回类型就是Boolean
with
函数
- 上下文对象:作为接收者(
this
) - 返回值:lambda 结果
使用场景:推荐使用 with
在上下文对象上调用函数,而不提供 lambda 结果。在代码中,我们可以将 with
理解为“对于这个对象,执行以下操作”。
示例:
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' 被调用,参数为 $this")
println("它包含 $size 个元素")
}
run
函数
- 上下文对象:作为接收者(
this
) - 返回值:lambda 结果
使用场景:run
在 lambda 中既可以初始化对象,又可以计算返回值。使用 run
我们可以进行空安全调用以及其他计算操作。
示例:
初始化和计算
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
-
链式空检查
val firstName: String? = null
var middleName: String? = null
var lastName: String? = null
middleName = "M "
lastName = "Vasava"
firstName?.run {
val fullName = this + middleName + lastName
print(fullName) // 仅打印 M Vasava
}
run源码分析:
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
- inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
- <T, R> T.run : T代表是要为T而扩展出一个函数名run(任何类型都可以 万能类型.run), R代表是Lambda表达式最后一行返回的类型
- block: T.() -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最后一行返回推断的类型
- : R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个run函数的返回类型就是Boolean
- T.() 是让lambda表达式里面持有了this(run函数), (T) 是让lambda表达式里面持有了it(let函数)
apply
函数
- 上下文对象:作为接收者(
this
) - 返回值:对象本身
使用场景:我们建议在不返回值的代码块中使用 apply
,主要用于操作接收者对象的成员。最常见的用例是对象配置。我们可以理解这样的调用为“将以下赋值应用于该对象”。
示例1:
val adam = Person("Adam").apply {
name = "Adam"
age = 20
city = "London"
}
println(adam)
示例2:
val dialog = AlertDialog.Builder(this).apply {
setTitle("警告!")
setMessage("这是一个警告对话框。")
}.create()
also函数
- 上下文对象:作为参数(it)。
- 返回值:对象本身。
使用场景:可用于需要引用对象而不是其属性和函数的操作,或者当您不想从外部作用域隐藏 this 引用时。 当在代码中看到also时,可以将其读作“并且还对对象执行以下操作”。