Kotlin 中的作用域函数

325 阅读5分钟

Kotlin 中的作用域函数

Kotlin 中自带的作用域函数主要包括 letrunwithapplyalso 。当然还存在 takeIftakeUnlessrepeat 函数,今天就来扒一扒下这几个函数的实现和区别

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 而不是 itlambda 表达式最后一行是你需要转换的对象类型,如果最后一行是调用方法,那么转换的对象类型是 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 表达式中获取到的是 thislambda 表达式最后一行是你需要转换的对象类型,如果最后一行是调用方法,那么转换的对象类型是 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 表达式中获取到的是 thisapply 会调用 函数类型参数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 返回 nulltakeIflambda 表达式中可以引用到当前对象,默认是用 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
}

可以看到 takeUnlesstakeIf 刚好相反,它会在 predicate 返回 truetakeUnless 返回 null。 在 predicate 返回 falsetakeUnless 返回当前对象

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 表达式会获取到当前遍历的索引

总结

letrunwithapplyalso 在很多时候都是可以替换使用的,那么在哪种情况下使用哪种作用域函数就很关键了。

这里就根据我本人平常的使用场景总结下

作用于函数使用场景
let非空对象的引用
runforEach 返回,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)
}

参考文档

Scope functions