对比Java学Kotlin作用域函数

Posted 陈蒙_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对比Java学Kotlin作用域函数相关的知识,希望对你有一定的参考价值。

什么是作用域函数?

首先,落脚点是函数,什么的函数呢?能在某个上下文对象(可能是普通对象,也可能是个 Unit)的作用域内执行代码的函数。这里的作用域和 Java 的作用域有所不同,Java 的作用域更多的是指一对闭合的 {} 的内部区域:

void func() {
    String variable;

    {
       String variable;
    }
}

变量独享其所在{}的区域,在这个区域内其他变量或方法都可以引用到这个变量,我们称之为作用域。
而 Kotlin 作用域函数的作用域,则更加灵活了,既可以用 lambda 表达式的方法引用上下文对象,也可以直接在上下文对象内部进行操作:

Person().apply {
    this.name = "zhangsan"
    this.age = 10
}

当然,不会使用作用域函数不影响使用 Kotlin,但是在某些情况下使用作用域函数会使我们的代码更加简洁,具有更好的可读性和可维护性。

使用方法

我们举个最常见的例子,有一个 Person 类如下:

class Person(val name: String, val age: Int, val addr: String) {
    fun moveTo(anotherAddr: String) {}
    fun incrementAge() {}
}

正常情况下我们可以执行如下操作:

val zhangsan = Person("Zhangsan", 20, "Shanghai")
zhangsan.moveTo("Suzhou")
zhangsan.incrementAge()

等效地,我们可以使用作用域函数将上面的操作改写如下:

val zhangsan = Person("Zhangsan", 20, "Shanghai").let {
    it.moveTo("Suzhou")
    it.incrementAge()
}

改写前后二者的执行结果完全一致。同时后者具有更好的可阅读性和简洁性,以及降低出错概率。之所以说可以降低出错概率,是因为往往我们在复制代码的过程中会手误造成错误,比如如下代码:

// zhangsan stuff
val zhangsan = Person("Zhangsan", 20, "Shanghai")
zhangsan.moveTo("Suzhou")
zhangsan.incrementAge()

// lisi stuff
val lisi = Person("Lisi", 20, "Shanghai")
zhangsan.moveTo("Shenzhen")
lisi.incrementAge()

当我们写完 zhangsan,习惯性的复制出 lisi 的代码,但是在改名的时候却遗漏了其中的 zhangsan.moveTo("Shenzhen"),从而导致代码运行出错,而使用作用域函数则可以很好的避免这个问题。这也是“代码越少,错误越少”的体现。

除了 let,还有其他几个作用域函数,其主要区别在于在作用域内能引用当前对象的方式以及返回值。

函数引用对象返回值是否为扩展函数备注
letitLambda 结果判空,不返回最后一行的值,而是返回整个对象,其余与 let 相同
runthislambda 结果在内部也能引用到当前对象的 this,该点与 apply 相同,不同点是返回最后一行的值
run-lambda 结果
withthislambda 结果等效于 run,相当于其另一种写法
applythis当前对象用于对象属性的赋值等操作,在内部能引用到当前对象的 this,以方便对属性赋值以及调用对象内部方法,返回整个对象
alsoit当前对象打印日志,不返回最后一行的值,而是返回整个对象,其余与 let 相同

使用场景

如下述代码:

var name : String ? = "ZhangSan"
if (!TextUtils.isEmpty(name)) {
    print(name.length)
}

IDE 会报错:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

这是因为,虽然我们给 name 赋了一个非空的值,但是由于代码块不是原子性的,不保证在执行 name.length 时 name 已经是 null 了,可能发生空指针异常。

这个时候,我们可以使用 let 函数对上述代码改造以避免报错:

var name: String? = "ZhangSan"
name?.let { 
    print(it.length)
}

之所以这么写不会报错,是因为 let 代码块里面的 it 变量是 final 的,我们可以运行如下代码进行试验:

var name : String ? = "zhangsan"
name?.let {
    it = null // 编译报错
    println(name == it) // True
    println(name === it) // True,说明二者是同一个引用
}

直接给 it 赋值会导致编译报错,报错信息如下:

Val cannot be reassigned
Null can not be a value of a non-null type String

也就是说,当执行完 ?. 之后传入 let {} 内的是 name 变量非空时的快照,就是一个值,不会为 null,且不能改变。

大家有没有联想到什么?是不是跟 Java 里面的匿名内部类引用外部的变量必须是 final 的原理有点类似?详见 Java 匿名内部类中的外部引用为什么必须是 final 的?

这种写法也一定程度上消灭了可恶的 if-else。

let

let 一般用于非空的对象上,比如上面我们提到的 name?.let{}。在 let 中引用上下文对象一般是通过 it 来实现,在 IDE 中我们可以清楚的看到提示:

但是为了更好的可读性,我们也可以自己指定名称:

    val zhangsan = Person("Zhangsan", 20, "Shanghai").let { person ->
        person.moveTo("Suzhou")
        person.incrementAge()
        println(person)
    }

注意,let 返回的是 lambda 表达式的结果,上述代码返回的是 Unit。如果我们想让 zhangsan 是 moveTo() 以及 incrementAge() 之后的 Person,我们应该:

    val zhangsan = Person("Zhangsan", 20, "Shanghai").let { person ->
        person.moveTo("Suzhou")
        person.incrementAge()
        person
    }

apply

apply 主要用于配置对象内的属性,比如初始化,在内部以 this 指针引用当前对象,或者我们也可以把 this 指针省略掉。因为其返回值是当前对象,所以特别适合在新建对象的时候使用,字面意思,apply 的意思是 “apply the following assignments to the object”,比如:

    val zhangsan = Person("Zhangsan", 20, "Shanghai").appy { 
        this.moveTo("Suzhou")
        this.incrementAge()
    }
    print(zhangsan)

also

使用 this 引用上下文对象,返回值是该对象本身。根据其含义 “and also do the following with the object.”,多用于打印日志或执行方法。

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

run

使用 this 引用上下文对象,返回值是 lambda 表达式的结果。
run 的作用与 with 类型,但是使用方法与 let 类似。适用场景包括既要在 lambda 内初始化对象,又要做一些方法执行的功能。

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

with

with 通过 this 引用对象,返回的是 lambda 的结果。最好使用 with 调用对象的方法,而不要返回值,with 的意思是 “with this object, do the following”:

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

注意,我们在讨论的过程中,是没有限定上下文对象非空的,即,在空对象上也能执行这些作用域函数:

    fun foo() {
        val name : String? = null
        name.let {println("let")}
        name.run {println("run")}
        name.apply {println("apply")}
        name.also {println("also")}
        with(name) {
            println("with")
        }
    }

输出:
let
run
apply
also
with

而如果我们加上对其中一个加上?,则对应的作用域函数不会执行:

    fun foo() {
        val name : String? = null
        name?.let {println("let")}
        name.run {println("run")}
        name.apply {println("apply")}
        name.also {println("also")}
        with(name) {
            println("with")
        }
    }

输出:
run
apply
also
with

作用域函数不仅可以作用在变量上,也可以作用在 Unit 上:

        fun foo() {
            val name : String? = null
            name.let {println("let")} // let 返回 Unit
                .run {println("run")}
                .apply {println("apply")}
                .also {println("also")}
            with(name) {
                println("with")
            }
        }

输出:
run
apply
also
with

takeIf & takeUnless

出了上述作用域函数,Kotlin 的标准库还提供了 takeIf() 和 takeUnless() 方法,以方便在链式调用中检查对象的状态。 如果为 true,takeIf 返回当前对象,否则返回 null。相反的,如果为 false,takeUnless 返回当前对象,否则返回 null:

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

takeIf 和 takeUnless 一般和作用域函数结合使用,先用 takeIf 和 takeUnless 过滤出我们关心的对象,然后使用 ?.let 去操作这些对象。

fun displaySubstringPosition(input: String, sub: String) {
    input.indexOf(sub).takeIf { it >= 0 }?.let {
        println("The substring $sub is found in $input.")
        println("Its start position is $it.")
    }
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

参考文章

以上是关于对比Java学Kotlin作用域函数的主要内容,如果未能解决你的问题,请参考以下文章

对比Java学Kotlin作用域函数

对比Java学Kotlin数据类

对比Java学Kotlin数据类

对比Java学Kotlin数据类

Kotlin作用域函数的使用经验

Kotlin作用域函数的使用经验