第十讲 Kotlin之作用域函数
前介
在 Kotlin 中提供了一种扩展函数,功能尤为的强大。又为我们提供了 Lambda 表达式,简化我们的开发。在这些基础上 Kotlin 为我们提供了一系列的作用域函数(本质上还是通过扩展函数实现 + Lambda 实现的)。
作用域函数
什么是作用域函数呢?我的理解就是所有的对象,都可以使用这些函数来简化我们的操作,减少中间变量的创建,让我们的功能可以链式方式实现。(纯属个人理解)
常见的作用域函数
接下来我们看看 Kotlin 为我们提供了那些常用的作用域函数呢?又有和区别呢?
run 操作符
run 操作符的的功能,对应 Lambda 表达式中的 this 就是作用域对象。返回值是 Lambda 表达式的返回值。看完这句话是不是一头雾水呀?大家直接看代码吧!
fun main() {
val data = Data("阿文", 18)
val reslut = data.run {
name = "阿文2"
"修改完成"
}
println(reslut)
println(data.name)
}
输出结果:
修改完成
阿文2
代码中我调用 data 对象的 run 方法,且传入了一个 Lambda 表达式,且在 Lambda 表达式中直接访问和修改了 data 的 name 属性,最后 Lambda 表达式的返回值是 修改完成。(思考为何 Lambda 表达式能直接访问 data 对 象的属性呢?)
想不到?那我们看下源码吧。
public inline fun <T, R> T.run(block: T.() -> R): R {
。。。
return block()
}
可以看到 run 方法的本质是通过扩展函数来实现的(所有的作用域函数都是通过扩展函数来实现)。首先通过定义了 2 个泛型 <T, R> 且没有加任何类型约束,也就是说任何类型都可以调用,对应 T 是作用域对象,R 是输出类型。
注意传入的 Lambda 表达式,又是一个 T 扩展函数(这就是为啥 Lambda 能直接访问作用域对象的属性原因),且 Lambda 表达式返回值的是 R 类型。
接下来看看函数体,就一句关键代码,调用了传入的 Lambda 表达式。且整个 run 函数,返回了 Lambda 的返回结果。
let 操作符
let 操作符,对应传入 Lambda 表达式的 it 对象就是作用域对象( it 代表的是 Lambda 第一个参数,Lambda 表达式篇中讲过哦),返回值是 Lambda 表达式的返回值。
fun main() {
val data = Data("阿文", 18)
val reslut = data.let {
it.name = "阿文2"
"修改完成"
}
println(reslut)
println(data.name)
}
输出结果:
修改完成
阿文2
看了上面的结果,我想大家也能猜到是如何实现的吧。
源码分析:
public inline fun <T, R> T.let(block: (T) -> R): R {
...
return block(this)
}
let 操作符和 run 操作功能类似,只是 let 操作符,对应传入的 Lambda 表达式不是 T 的扩展函数,而第一个参数是 T 类型,且传入了作用域对象。
apply 操作符
apply 操作的的功能,对应 Lambda 表达式是作用域对象的扩展函数,返回值是作用域对象本身(注意这里哦,前面讲的 run 和 let 返回值都是 Lambda 表达式的返回值)。
data class Data(var name: String, var age: Int)
fun main() {
val data = Data("阿文", 18)
val reslut = data.apply {
name = "阿文2"
"修改完成"
}
println(reslut)
println(data.name)
}
输出结果:
Data(name=阿文2, age=18)
阿文2
源码分析:
public inline fun <T> T.apply(block: T.() -> Unit): T {
。。。
block()
return this
}
可以看到 apply 对应传递 Lambda 表达式是 T 的扩展函数(和 run 一样),但是返回值不在是 Lambda 表达式的返回值,而是 作用域对象 this 。
also 操作符
also 操作符的的功能,对应 Lambda 表达式的不是 T 的扩展函数,第一个参数是 T 类型(和 let 一样),返回值是 作用域对象 this (和 apply 一样)。
fun main() {
val data = Data("阿文", 18)
val reslut = data.also {
it.name = "阿文2"
"修改完成"
}
println(reslut)
println(data.name)
}
输出结果:
Data(name=阿文2, age=18)
阿文2
源码分析:
public inline fun <T> T.also(block: (T) -> Unit): T {
。。。
block(this)
return this
}
可以看到 also 对应传递 Lambda 和 let 一样 Lambda 接收一个 T 类型的参数,但是返回值不在是 Lambda 表达式的返回值,而是 this。
takeIf 操作符
判断 Lambda 表达式返回若为 ture,就会返回 this 对象,否则就是 null。
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
。。。
return if (predicate(this)) this else null
}
这用什么用呢?其实就是配合 Kotlin 中的 ? ,来判断对象是否为 null。比如说我们写代码很喜欢类似 RxJava的链式写法,但是有时候我想判断一个对象在一定条件下才可执行后续操作,为了不打断链式结构,可以使用此操作符。
举个🌰
/**
* 实战例子:
* 如果我们要做个登录需求,若输入的账号密码是空字符串,就输出 请输入账号密码
*/
data class User(val user: String, val pass: String)
fun main() {
val users = User("阿文", "18岁")
/**
* 可以看到,这里就使用了 takeIf 操作符来做判断.若 Lambda 返回的是 false ,返回对象就是 null 类型,在配合 ? 就直接返回了
*
* 再配合 ?: 就能完成操作了,是不是很精简,链式调用没有打破.
* 代码阅读性吗?看习惯就好啦!
*/
users
.takeIf {
it.user.isNotBlank() && it.pass.isNotBlank()
}
?.let(::callLogin) ?: println("请输入账号密码")
}
fun callLogin(user: User) {
println("登录 $user")
}
takeUnless 表达式
若 Lambda 表达式返回 false ,返回的是 this 对象,否则就是 null (与 takeIf 相反),大家自行理解吧。
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
。。。
return if (!predicate(this)) this else null
}
我感觉就用一个
takeIf就行啦,不然一会脑袋就乱了。
补充
不知道大伙有没有注意到,所有的作用域函数都是通过 inlin (内联函数)修饰。大伙想想为啥要这样呢?
其实这就要回想到 内联函数 的功能了。作用域函数在 Kotlin 中经常使用的,它的原理又是 扩展函数 + Lambda 表达式来实现。由于扩展函数会生成新的函数,Lambda 表达式又会生成新的 FunctionX 类(前面都讲过哦),为了减少这些不必要的开销,就将所有的作用域函数通过 inlin (内联函数)修饰了(为啥通过 inlin 修饰就能解决开销问题呢?自行看 特有函数篇 )。
总结
常用的几个作用域函数就是这些啦?当然还有好几个,我这里就不一一讲解了,原理都差不多。反正我常用的就这几个。
大家通过对比记忆比较好,我的技巧是 let 和 run 对比记忆,apply 和 also 对比记忆。
| 操作符 | 功能 |
|---|---|
| let | Lambda 表达式的只有一个参数是对应对象,返回值是 Lambda 的返回值 |
| run | Lambda 表达式属于对应对象的扩展函数,返回值是 Lambda 的返回值 |
| apply | Lambda 表达式属于对应对象的扩展函数,返回值对应对象 |
| also | Lambda 表达式的只有一个参数是对应对象,返回值是对应对象 |
| takeIf | Lambda 表达式的只有一个参数是对应对象,返回值若 Lambda 表达式 true,返回 this,否则返回 null |
| takeUnless | Lambda 表达式的只有一个参数是对应对象,返回值若 Lambda 表达式 false,返回 this,否则返回 null(和 takeIf 相反) |
其实还有一个with的作用域函数,但是我觉得这个东西很鸡肋(完全和run的功能一样)。如果想学习,就看下源码吧
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}