「Kotlin作用域函数 - Scope Functions」
一、Scope Functions
Kotlin标准库中的作用域函数,主要包括let、run、with、apply、also,函数本身不难,但是他们太过于相似了,并且很多地方是可以相互通用的。并没有应对特定的场景做出规定,这就需要开发者熟练的掌握每一种函数的特性,深刻的理解才能够灵活的使用。
二、总览
看具体的函数之前,先上一张表格,简单的总结一下,各个函数的基本信息,包括对象引用、函数的返回值,是否是扩展函数等:
函数 | 扩展函数 | 返回值 | 对象引用 | Lambda定义 | 显/隐式(in block) |
---|---|---|---|---|---|
let | 是 | R(from block body) | it | (T) -> R | explicit(显) |
run | 是 | R(from block body) | this | T.() -> R | implicit(隐) |
run | 否 | R(from block body) | - | () -> R | - |
with | 否 | R(from block body) | this | T.() -> R | implicit(隐) |
apply | 是 | T(this) | this | T.() -> R | implicit(隐) |
also | 是 | T(this) | it | (T) -> Unit | explicit(显) |
-
从返回的结果来区分,可以发现只有apply、also的返回值是本身,而其他诸如let、run、with的返回值均为作用域内执行的Lambda(block)函数的值,那么这个值就存在不确定性,因为返回值为R(from block body),则可以是Unit,可以是本身Value,可以是操作过后的其他值。简单点理解:就好比在这个有限的作用域内(block),对于传入的对象引用it或者this,做一些附加操作或者变换获得想要的返回值。
-
从引用对象来区分,内部隐式(implicit)的使用this的函数可以分为run、with、apply,而also、let内部则显式(explicit)使用了it,并且他们还有一个共同的点:当对象引用为this时,通常这个this都是作为receiver,block方法的调用方式为T.() -> R,而对象引用为it时,此时一般将it作为参数argument来调用block:(T) -> R.
还是以一个简单的表格来总结一下:
返回值/对象引用 | this | it |
---|---|---|
Self | apply | also |
Result(block) | run/with | let |
三、具体分析
1.Let、Run函数
let函数内部的对象引用为it,返回值为Lambda表达式的值,并且是扩展函数;一般在实际开发过程中更多的是作为非空对象的判断并执行相应的操作。当然调用可以是链式的,侧重点是为了使代码简洁。看一下测试代码:
//定义一个UserVO对象,包括三个属性字段
data class UserVO(var name: String, var age: Int = 20, var lesson: String = "Kotlin") {
fun study(lessonName: String) {
lesson = lessonName
}
fun modifyName(name: String) {
this.name = name
}
}
fun main() {
letMethodTest()
}
//测试let函数
fun letMethodTest() {
var userVO: UserVO? = UserVO("Sai")
val blockResult = userVO?.let {
println(it)
it.study("Java")
it.modifyName("皮皮虾")
println("-----------------")
println("After modify : $it")
}
println("The result of let method is : $blockResult")
}
//打印结果信息
UserVO(name=Sai, age=20, lesson=Kotlin)
-----------------
After modify : UserVO(name=皮皮虾, age=20, lesson=Java)
The result of let method is : kotlin.Unit
Process finished with exit code 0
在let内部最后一条打印println("After modify : $it")
,由于println的返回值类型为Unit,因此最外层的打印语句的结果即为kotlin.Unit
。回忆之前的run函数好像与let的区别不大,看一下run函数的实现:
其与let区别在于,run中的引用对象为this,且这个引用的对象是作为receiver,let中it则是作为参数argument。到这里似乎还是不够清晰,对于上述的测试代码,使用run函数一样可以实现:
var userVO: UserVO? = UserVO("Sai")
val blockResult = userVO?.run {
modifyName("皮皮虾")
study("Python")
println("-----------------")
println("After modify : $this")
}
println("The result of let method is : $blockResult")
run中调用属性方法时,是可以隐式调用的,省略了this.xx,可见run与let一样同样可以null-check,官方文档建议当对象存在初始化并且有计算的结果需要返回时使用run,其实let可以做同样的事情,但是写法上会稍有区别,从功能上却没有差异。如下代码:
输出的结果是一样的没有任何差异,而run的隐式调用来的更加简洁,由于let的引用对象为it,因此在调用count时,需要修改其局部变量名称。对于这种细微的差别,可以根据实际需要灵活的使用。再来看看源码中分别对run、let的注释部分:
- let -> Calls the specified function block with this value as its argument and returns its result.
- run -> Calls the specified function block with this value as its receiver and returns its result.
argument很好理解,将value作为参数传入到block的lambda表达式中,而什么是receiver是什么呢?通俗的讲,当我们在调用方法时,形如someInstacn.someFunction(),这里的someInstance则是receiver,回忆Java中,内部调用某一个方法时this.someFunction(),这里的this是可以省略的,类推到run函数中,由于with this value as its receiver,那么对于方法 .f() 则可以直接调用,对于let中则可以写成it.f()。程序的编写主要是为了简洁易读,那么对于这种计算、调用虽然都能完成正确的结果展示,但是则更倾向于使用run。
2. Apply、Also函数
Apply函数与Also函数比较类似,也是可以通用的,返回值同样为本身Self,但是内部引用的对象会有区别,分别为this、it。前面提到了this与it的区别会影响到是作为receiver还是参数argument。对于apply官方给出的使用建议为apply the following assignments to the object. 字面意思很简单,为对象做一些附加操作,如赋默认值,设置属性等。而also也有同样的含义and also do the following with the object. 后者的侧重点在于,使用该对象做一些额外的事情 而前者的侧重点在于为该对象做一些操作。来看一段测试的代码:
fun testAlsoAndApply() {
val numberList = mutableListOf<Int>()
numberList.also {
it.add(2)
println("In also print the list : $it")
}.apply {
add(4)
add(9)
add(5)
println("In apply print the list : $this")
}.also {
println("Sort the list")
}.sort()
println(numberList)
}
//打印的信息
In also print the list : [2]
In apply print the list : [2, 4, 9, 5]
Sort the list
[2, 4, 5, 9]
Process finished with exit code 0
可以看到,无论是also还是apply都可以做相同的操作,无论是添加元素,还是删除元素,仅仅是内部函数调用上的区别。但对于添加元素apply来的更加简洁,因为内部的引用对象为this之前提到的,对于receiver的调用有着自身的优势,而添加元素可以看作是对数组做一些操作,而打印信息则可以看成使用该数组做一些操作,当然apply中也可以打印,只不过是it与this的区别。这仅仅是一种建议,并不是说是强制的一定要去这么做,只是考虑代码结构的简洁、易读性。
3.With函数
With函数不属于扩展函数,其内部引用的对象为this而返回值为block的返回值这里跟apply的主要区别就是返回值与调用方式:
fun testWith() {
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," + " the last element is ${last()}"
}.uppercase()
println(firstAndLast)
}
//log
THE FIRST ELEMENT IS ONE, THE LAST ELEMENT IS THREE
Process finished with exit code 0
官方的说法with this object, do the following.,我感觉同apply的意思是差不多的。但是它没有返回本身,而是返回了block的值。这是主要差别。
四、怎么用?
多写,多感受,这当然是废话😄,但是写的过程中多加思考是很有必要的,作用域函数真的相似点有很多,并且很多时候都是可以通用的,跟团队的规范,个人习惯也有很大的关系,但是目的是确定的,简洁、易读。网上也有很多同行在总结这个,给一张图作为使用参考:
当然仅仅是作为一种参考,具体的使用还是需要结合实际情况的。原文地址戳这里CLICK
五、一些建议
原文地址Kotlin Standard Functions cheat-sheet
六、链接
关于Kotlin中receiver的讨论Stackoverflow
Kotlin作用域函数使用建议
官方文档Scope Functions
Example of when should we use run, let, apply, also and with on Kotlin