Kotlin-作用域函数

522 阅读7分钟

第十讲 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 表达式中直接访问和修改了 dataname 属性,最后 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 表达式是作用域对象的扩展函数,返回值是作用域对象本身(注意这里哦,前面讲的 runlet 返回值都是 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 对应传递 Lambdalet 一样 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 修饰就能解决开销问题呢?自行看 特有函数篇 )。

总结

常用的几个作用域函数就是这些啦?当然还有好几个,我这里就不一一讲解了,原理都差不多。反正我常用的就这几个。

大家通过对比记忆比较好,我的技巧是 letrun 对比记忆,applyalso 对比记忆。

操作符 功能
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()
}