并非意志坚强就可以无所不能,人世不是那么单纯的。老实说,我甚至觉得每天坚持跑步同意志强弱并没有太大关联。我能够坚持跑二十年,恐怕还是因为合乎我的性情,至少“不觉得那么痛苦”。人生来如此,喜欢的事自然可以坚持下去,不喜欢的事怎么也坚持不了。 ——当我谈跑步时我谈些什么

想要掌握Kotlin,域函数是不得不迈过的一道坎。

所谓“域函数”

一句话,域函数(scope functions)是为给定的对象创建一个临时的域,在这个域中执行一些操作。相比于传统的对象-函数调用写法,域函数在减少代码量的同时,还可以让编码在逻辑上看起来更加清晰,便于扩展和维护。

对比一下域函数写法与普通写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 域函数写法
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}

// 普通写法
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

域函数要结合lambda表达式使用。Kotlin中一共有五个域函数:let, run, with, apply, also。这些域函数有两个区别点。

  • 域函数中如何引用上下文对象
  • 域函数返回值

区别点:上下文对象

域函数中用this或者it指代上下文对象。

this

run, withapply在lambda表达式中用this指代上下文对象,就好像lambda表达式是在对象内部调用的一样,this是可以省略的。对于调用对象内部方法、属性的代码,应当选择使用this指代的域函数。如果在域内要调用其他对象的函数,不要选择this指代,因为容易弄混。

1
2
3
4
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20
city = "Hangzhou"
}

it

let, also在lambda表达式中用it指代上下文对象,与this不同,在访问方法和对象时it是不能省略的。当上下文对象需要在域内充当函数参数时,就选用it类型的域函数。另一个便捷之处在于,可以为it指代别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}

val i = getRandomInt()

// 为it指代别名
fun getRandomInt(): Int { value ->
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $value")
}
}

区别点:返回值

Lambda表达式结果

let, runwith返回lambda表达式的结果(最后一行),可以用这些域函数来进行赋值,也可以进行链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 赋值
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") } // 返回count数
}
println("There are $countEndsWithE elements that end with e.")

// 无视返回值,只是执行域函数内部操作
val numbers = mutableListOf("one", "two", "three")
with(numbers) { // with是this指代,可省略
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}

上下文对象

also, apply返回上下文对象,可以继续对此进行链式调用,也可以直接返回上下文对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 链式调用
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply { // 注意apply使用this指代(省略掉)
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

// 作为返回值
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it") // it指代上下文对象
}
}

val i = getRandomInt()

逐个函数讲解

let

上下文对象it返回值是lambda表达式计算结果(最后一行)。

let可以作为链式调用中的一环来使用。

1
2
3
4
5
6
7
8
9
// 链式调用
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}

// 更加精简
numbers.map { it.length }.filter { it > 3 }.let(::println)

let经常用来在非空对象上执行一系列操作。

1
2
3
4
5
6
7
val str: String? = "Hello"   
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}

let另一个用法是为变量it创建别名,以增强代码可阅读性。借助于IDE,通常我们可以看到it指代的是什么对象,对这个用法的需求并非十分强烈。

1
2
3
4
5
6
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")

with

with不是扩展函数,它接收上下文对象作为函数参数,在lambda表达式中用this指代,返回结果是lambda表达式的值。

建议在使用with时不要处理它的返回结果,这样with就可以根据字面含义简单的理解成“在这个对象上进行如下操作”。

1
2
3
4
5
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}

另一种用法是创建一个辅助对象,它的属性或者方法可以用来计算出某个值。

1
2
3
4
5
6
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) { // 相当于声明了一个局部函数
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)

run

使用this指代上下文对象,返回结果是lambda表达式值。

run在含义上与with一致,在调用方式上与let一致。当需要进行对象初始化、并同时要返回一个计算结果时,应当使用run

1
2
3
4
5
6
7
8
9
10
11
12
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

除了在接收者对象上调用以外,run还可以让我们在需要一个表达式的地方传入一个代码块(这种用法略复杂)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"

Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) { // findAll正则匹配
println(match.value)
}

/**
* output:
* +1234
* -FFFF
* -a
* be
*/

apply

使用this指代上下文对象,返回值是上下文对象本身。

apply适用的场景是不需要返回值,且主要是操作对象成员的过程。常见的就是对象配置,“在对象上进行如下操作”。

apply可以很容易地变成链式操作。

1
2
3
4
val adam = Person("Adam").apply {
age = 32
city = "London"
}

also

使用it指代上下文对象,返回值是上下文对象本身。

also的使用场景是将对象作为一系列操作的参数,在这些操作中不应该对参数产生副作用,因此你可以在一个链式调用中很方便地加上also,或者从链式调用中把also摘掉,且不影响原有逻辑。

当你在代码中看到also时,可以将其理解为“还需要做这些事情”。

1
2
3
4
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

总结

用一张表格列出函数的对象引用与返回值。

Function Object reference Return value Is extension function
let it Lambda result Yes
run it Lambda result Yes
run - Lambda result No: called without the context object
with this Lambda result No: takes the context object as an argument
apply this Context object Yes
also it Context object Yes

一个简单的函数选用指南如下,它们的应用场景有重叠的部分,使用时应当具体情况具体分析。

  • 在非空对象上调用lambda表达式:let
  • 在域内将表达式抽象成一个变量:let
  • 对象配置:apply
  • 对象配置并计算结果:run
  • 执行一系列表达式,非扩展函数:run
  • 附加效果:also
  • 将对于某个对象的函数调用组合:with

尽管作用域函数功能强大,在使用时应当谨慎,避免出错,尤其是嵌套的情况应当越少越好;当你在写链式调用时,一定要小心分辨当前的上下文对象。

参考