Kotlin中 run, with, let, also and apply 函数的分类与对比

Kotlin提供了run, with, let, also and apply等功能函数,使用这些功能函数可以提高代码的可读性和简洁性。比如如下的代码利用java实现是这样的 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static int getPeopleTotalAge(People people) {
if (people == null) {
return 0;
}
int age = people.age;
if (people.child1 != null) {
age += people.child1.age;
}
if (people.child2 != null) {
age += people.child2.age;
}
return age;
}

如果用Kotlin来实现,就能变得很简洁:

1
2
3
fun getPeopleTotalAge(people: JavaTest.People?) = people?.run {
(child1?.age ?: 0) + (child2?.age ?: 0) + age
} ?: 0

Kotlin中 run, with, let, also and apply在使用上有很多区别,我把这几个函数又能分成如下三类。

三种类型

1.普通函数与扩展函数

with 和 T.run 很类似,比如下面的代码中,实现的功能一样 :

1
2
3
4
5
6
7
8
9
with(people.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
people.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}

但是区别在于 with 是一个普通函数,但是T.run 是一个扩展函数,那么用哪个比较好呢? 假设 people.settings 可能为null,那上面的代码应该修改为如下 :

1
2
3
4
5
6
7
8
9
10
11
// 比较繁杂
with(people.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// 比较简洁
people.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}

所以根据具体使用场景选择with 和 T.run,可以使代码更简洁。另外要注意一下,run 函数也有普通函数的版本,与T.run 使用上没有什么区别,比如下面的代码 :

1
2
3
4
5
6
fun testRun(i: Int) = run {
if (i < 0) {
return 0
}
i + 1
}

2.this参数与it参数

T.run 与 T.let 也很类似,比如下面的代码中,实现的功能一样 :

1
2
3
4
5
6
7
stringVariable?.run {
println("The length of this String is $length")
}
stringVariable?.let {
println("The length of this String is ${it.length}")
}

从代码中明显区别是T.run 可以直接调用属性,而T.let需要通过it来调用属性,如果我们查看这两个函数的源码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

可以发现,T.run 是通过扩展的方式来传递参数 block: T.() -> R;但是T.let 是传递一个函数 block: (T) -> R。所以T.run可以直接调用,但是T.let需要it调用。虽然T.run 看起来比T.let 更好用,但是在一些情况下更适合用T.let :

  • T.let 明确了参数的关系,在调用参数时不会和其它全局变量搞混;
  • T.let 中的it 也可以换名字,提高代码的可读性;
    1
    2
    3
    4
    stringVariable?.let {
    nonNullString ->
    println("The non null string is $nonNullString")
    }

另外,T.apply 与 T.also 也一样,前者可以直接调用参数,后者需要通过it来调用参数。比如下面的代码 :

1
2
3
4
5
6
7
stringVariable?.apply {
println("The length of this String is $length")
}
stringVariable?.also {
println("The length of this String is ${it.length}")
}

3.返回this或返回最后一行

T.let 和 T.also 使用上是差不多的,通过前面的分析我们知道都是通过it来调用参数。但是在下面的例子中,它们还是有些许不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
val original = "abc"
original.let {
println("The original String is $it") // "abc"
it.reversed()
}.let {
println("The reverse String is $it") // "cba"
it.length
}.let {
println("The length of the String is $it") // 3
}
//错误,一直返回"abc"
original.also {
println("The original String is $it") // "abc"
it.reversed()
}.also {
println("The reverse String is ${it}") // "abc"
it.length
}.also {
println("The length of the String is ${it}") // "abc"
}
//正确,结果与调用T.let一致
original.also {
println("The original String is $it") // "abc"
}.also {
println("The reverse String is ${it.reversed()}") // "cba"
}.also {
println("The length of the String is ${it.length}") // 3
}

T.also 返回的是this,即”abc”,所以像T.let链式调用的时候,一直返回”abc”,导致代码逻辑错误。T.also比较适合运用于链式调用,比如AlertDialog.Builder创建对话框时的调用方式。混合运用T.let和T.also能使代码更加简洁,比如下面的代码 :

1
2
3
4
5
6
7
8
9
// 一般实现方式
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// 改进后的方式
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

同理,T.apply 和 T.run 也是这种类型的组合,T.apply 返回this,而T.run 返回的是最后一行。

总结

通过前面的分类对比,我们知道这些函数可以分成三类:

  • 普通函数与扩展函数 (with 与 T.run)
  • this参数与it参数(T.run 与 T.let、T.apply 与 T.also)
  • 返回this或返回最后一行(T.let 和 T.also、T.apply 与 T.run)

我们可以整理出一个表格,明确了各个函数的使用方式:

\ 是否扩展函数 调用参数方式 返回值
with this 最后一行
T.run this 最后一行
T.let it 最后一行
T.apply this this
T.also it this