Kotlin 中的作用域函数
Kotlin 中自带的作用域函数主要包括 let 、 run 、 with 、apply 和 also 。当然还存在 takeIf 、takeUnless 和 repeat 函数,今天就来扒一扒下这几个函数的实现和区别
let
源码实现
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
可以看到 let 会将对象类型进行转换,并且函数类型参数 block 会返回 当前对象的引用,所以 lambda 表达式中可以引用到当前对象,默认是用 it 表示当前对象。lambda 表达式最后一行是你需要转换的对象类型,如果最后一行是调用方法,那么转换的对象类型是 Unit
例如我们需要将数据类转换成其他类型时,可以这样做
val myData: MyData = MyData("linpopopo", 28)
// 将MyData类型转换成Pair,当然一般我们不会去显式的声明对象的类型
val pair: Pair<String, Int> = myData.let {
it.name to it.age
}
//不需要显式声明对象类型,Kotlin会去判断当前的对象类型
val pair = myData.let { it.name to it.age }
// 隐式返回一个kotlin.Unit的对象,既然是函数调用我们不需要获取到当前变量
val unit = myData.let { println(it.age) }
data class MyData(var name: String, var age: Int)
run
源码实现
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
// run 顶层函数
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
可以看到 run 会将对象类型进行转换,并且函数类型参数 block 的作用域是当前对象,所以 lambda 表达式中获取到的是 this 而不是 it。lambda 表达式最后一行是你需要转换的对象类型,如果最后一行是调用方法,那么转换的对象类型是 Unit
将上面的例子改写一下
// 将MyData类型转换成Pair,当然我们的作用域是当前对象,所以我们不需要显式调用this
val pair = myData.run { this.name to this.age }
// 不需要显式调用this
val pair2 = myData.run { name to age }
// 隐式返回一个kotlin.Unit的对象,既然是函数调用我们不需要获取到当前变量
val unit = myData.run { println(age) }
with
源码实现
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
可以看到 with 不是一个拓展函数,而是一个顶层函数,为什么说是一个顶层函数?因为它在任何时刻都可以被调用,并不需要像拓展函数一样使用 T.expansionFunction 表达式调用。
with 也会将对象类型进行转换,函数类型参数 block 的作用域是当前对象,所以 lambda 表达式中获取到的是 this 。lambda 表达式最后一行是你需要转换的对象类型,如果最后一行是调用方法,那么转换的对象类型是 Unit
将上面的例子改写一下
val pair = with(myData) { name to age }
// 隐式返回一个kotlin.Unit的对象,既然是函数调用我们不需要获取到当前变量
val unit = with(myData) {
println(age)
}
apply
源码实现
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
可以看到 apply 作用域是当前对象,所以 lambda 表达式中获取到的是 this。 apply 会调用 函数类型参数block并返回当前对象,所以 apply 并不会将对象类型进行转换
val applyData = myData.apply {
name = "lisa"
age = 29
}
println(applyData) // MyData(name=lisa, age=29)
println(applyData === myData) // true
also
源码实现
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
函数类型参数 block 会返回 当前对象的引用,所以 lambda 表达式中可以引用到当前对象,默认是用 it 表示当前对象。 also 会调用 函数类型参数block并返回当前对象,所以 also 并不会将对象类型进行转换
val alsoData = myData.also {
it.name = "lisa"
it.age = 29
}
println(alsoData) // MyData(name=lisa, age=29)
println(alsoData === myData) // true
takeIf
源码实现
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (predicate(this)) this else null
}
takeIf 需要传入函数类型参数 predicate 并且返回 Boolean,如果 predicate 返回 true,则 takeIf 返回当前对象;如果 predicate 返回 false,则 takeIf 返回 null。takeIf 的 lambda 表达式中可以引用到当前对象,默认是用 it 表示当前对象
val adultData = myData.takeIf { it.age > 18 }
val englishName = myData.takeIf { Pattern.matches("[a-zA-Z]", it.name) }
println(adultData) // MyData(name=linpopopo, age=28)
println(englishName) // null
takeUnless
源码实现
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (!predicate(this)) this else null
}
可以看到 takeUnless 和 takeIf 刚好相反,它会在 predicate 返回 true,takeUnless 返回 null。 在 predicate 返回 false,takeUnless 返回当前对象
val adultData = myData.takeUnless { it.age > 18 }
val englishName = myData.takeUnless { Pattern.matches("[a-zA-Z]", it.name) }
println(adultData) // null
println(englishName) // MyData(name=linpopopo, age=28)
repeat
源码实现
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }
for (index in 0 until times) {
action(index)
}
}
repeat 其实很简单,就是在内部维持一个 for 循环,每次循环的索引会返回给 action,即 lambda 表达式会获取到当前遍历的索引
总结
let 、 run 、 with 、apply 和 also 在很多时候都是可以替换使用的,那么在哪种情况下使用哪种作用域函数就很关键了。
这里就根据我本人平常的使用场景总结下
| 作用于函数 | 使用场景 |
|---|---|
| let | 非空对象的引用 |
| run | forEach 返回,if 判断 |
| with | 复杂对象并且多次调用对象的方法或成员变量 |
| apply | 对象的配置 |
| also | 附加一些新功能,例如单例的实现,或者文件IO流读写 |
let 的实例代码
val activity = WeakReference<Activity>(this)
activity.get()?.let { it.customFunction() }
run 的实例代码
// forEach退出
run {
listOf(0, 1, 2, 3).forEach {
if (it == 1) return@run
println(it)
}
}
// if判断
if (myData.run { name == "linpopopo" && age > 18 }) {
println("linpopopo成年了")
}
with 的实例代码
with(WithTest()) {
A()
B()
C()
D()
E()
F()
G()
}
class WithTest {
fun A() {}
fun B() {}
fun C() {}
fun D() {}
fun E() {}
fun F() {}
fun G() {}
}
apply 的实例代码
val intent = Intent().apply {
action = "com.linpopopo.action"
component = ComponentName(this@MainActivity, OtherActivity::class.java)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
also 的实例代码
// 双重检查单例
class Singleton {
companion object {
@Volatile
private var singleton: Singleton? = null
fun getInstance(): Singleton {
return singleton ?: synchronized(this) {
singleton ?: Singleton().also { singleton = it }
}
}
}
}
// 文件IO流读写
val file = File("is.txt")
val fos = FileOutputStream("os.txt")
val fis = FileInputStream(file)
var len: Int
val buffer = ByteArray(1024)
while (fis.read(buffer).also { len = it } != -1) {
fos.write(buffer, 0, len)
}