Kotlin中Standard.Kt文件中短短不到100来行库函数源码,但是它们作用是非常强大,可以说它们是贯穿于整个Kotlin开发编码过程中。Kotlin 标准库包含几个函数(run、with、let、also和apply),它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数,使用它们能让你的代码会更具有可读性、更优雅、更简洁。
标准库函数作用:
举个简单的例子:
假如一个方法需要对传入的Book修改价格和作者,然后将书名倒叙排列加为目录路径去生成一个名为abc文件(此奇葩操作仅为实例),返回生成文件夹和文件是否成功。
普通模式:
fun makeDir(book: Book): Boolean {
book.price = 30
book.anthor = "马丁"
val resultPath = "${book.name.reversed()}/abc"
val result = File(resultPath)
return result.mkdirs()
}
使用标准库函数模式:
fun makeDirImprove(book: Book) = book
.run {
price = 30
anthor = "马丁"
}
.let {
val name = book.name.reversed()
"${name}/abc"
}
.also {
File(it).mkdirs()
}
将Book的属性修改、处理文件路径、生成文件的逻辑抽离,一步一步分隔在不同的临时代码区域,很容易区分哪块代码是写什么逻辑的。
可以看出,通过标准库函数模式可以将每一个逻辑拆分为一个个作用域(block)单独处理,使得原本分散的代码逻辑变成相对独立的逻辑一环扣一环的链式调用,同一块的代码逻辑分布更加内聚,可读性更高。当代码逻辑更加复杂的情况下,这种优势更加明显。
标准库函数区别
常见的标准库函数有run、with、let、also和apply,用法也是非常简单,但是这些库函数有难点在于它们的用法都非常相似,有的人甚至认为有的库函数都是多余的,其实不然,每个库函数都是有它的实际应用场景。
可以从三个维度区分它们:
1.是否是扩展函数
2.在block内部用this还是it指代调用者对象本身
3.返回值,是否返回调用者对象本身,还是返回block的返回值
是否是扩展函数
如果是扩展函数,即函数定义是T.函数名,则可以用对象直接调用该函数,在外层就可以进行空安全检查,只有非空时才能进入函数块内对book进行操作。
例如let:
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
因为定义为T.let,所以是扩展函数,所以可以对象直接调用该函数,而且支持外层进行空安全检查,所以block内部的it不需要进行空判断,例如:
book?.let {
it.name = "《计算机网络》"
it.anthor = "牛顿"
it.price = 60
}
类似的还有apply、also(run既有扩展函数也有非扩展函数的定义)
与let相对应的,例如with:
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
由于不是扩展方法,所以要将对象作为参数传入with方法,并且要在block内部对调用对象进行判空:
with(book) {
this?.name = "《计算机网络》"
this?.price = 40
print(this?.name ?: "《计算机网络》")
print(this?.price ?: 40)
}
类似的还有非扩展函数定义的run。
在block内部用this还是it指代调用者对象本身
是否传递this或it做为参数取决于block的定义类型,如果是扩展函数,即类似T.() -> Unit这种类型,则可以在block内部使用this指代调用者对象本身,而this是可以省略的,所以更加方便。如果block定义为类似**(T) -> Unit**,则可以用it指代调用者对象本身,it不能省略,但是好处是可以改用其他名字指代,这在嵌套调用标准库函数的时候对于可读性非常有帮助。
还是以let举例:
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
参数为block: (T) -> R,说明block不是扩展函数,所以block内部不能用this指代调用对象,默认用it指代:
book?.let {
it.name = "《计算机网络》"
it.anthor = "牛顿"
it.price = 60
}
it可以改为其他名字,以便于在嵌套函数的情景进行区分:
book?.let {book->
book.name = "《计算机网络》"
book.anthor?.let {anthor->
print(anthor)
}
}
类似的还有also。
与之相对应的比如apply:
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
注意到参数为block: T.() -> Unit是扩展方法,所以可以在block内部用this指代调用对象,而且this可以省略:
book?.apply {
name = "《计算机网络》"
price = 40
anthor = "马丁"
}
可以看到用this指代非常合适在block内部做对象的初始化工作,看起来很简洁清晰。
但是局限性在于this不能改为其他命名,所以当嵌套时容易混淆this的指向:
book?.apply {
this.name = "《计算机网络》"
this.price = 40
this.appendix.apply {
this.name = "《计算机网络附录》"
this.price = 4
this.appendix.apply{
this.name = "《计算机网络附录2》"
this.price = 1
}
}
}
类似的还有with、扩展函数定义的run。
返回值
返回值一般是返回调用者本身或者是返回block的返回值,前者可以方便地链式对一个对象多次进行不同逻辑块的处理,后者是可以不断对一个对象进行演进处理(即每次调用是对上一个调用的返回值)。
还是let举例:
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
返回的是block的返回值,这说明了可以通过链式调用去不断演进处理上一个调用的结果,例如:
val original = "abc"
// Evolve the value and send to the next chain
original.let {
println("The original String is $it") // "abc"
it.reversed() // evolve it as parameter to send to next let
}.let {
println("The reverse String is $it") // "cba"
it.length // can be evolve to other type
}.let {
println("The length of the String is $it") // 3
}
与之对应的例如also:
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
返回的是调用对象本身,所以每一步处理的都是对象本身,一般用于多个独立逻辑多次对一个对象进行处理的链式调用:
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
}
总结
标准库函数的作用是使得代码逻辑更加清晰可读性更高。它们很相似,容易混淆。要区分不同的标准库函数,没必要死记,只要根据上面的三个维度:
1.是否是扩展函数
2.在block内部用this还是it指代调用者对象本身?
3.**返回值是否返回调用者对象本身?
针对以上三个维度,只要分析函数定义即可找到它们的区别,,再根据不同的需要选择合适的函数即可。
原创不易,如果觉得本文对自己有帮助,别忘了随手点赞、评论和关注,这也是我创作的最大动力~
对音视频开发感兴趣的朋友也可以看下 音视频系统学习的浪漫马车之总目录
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。