对比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,还有其他几个作用域函数,其主要区别在于在作用域内能引用当前对象的方式以及返回值。
函数 | 引用对象 | 返回值 | 是否为扩展函数 | 备注 |
---|---|---|---|---|
let | it | Lambda 结果 | 是 | 判空,不返回最后一行的值,而是返回整个对象,其余与 let 相同 |
run | this | lambda 结果 | 是 | 在内部也能引用到当前对象的 this,该点与 apply 相同,不同点是返回最后一行的值 |
run | - | lambda 结果 | 否 | |
with | this | lambda 结果 | 否 | 等效于 run,相当于其另一种写法 |
apply | this | 当前对象 | 是 | 用于对象属性的赋值等操作,在内部能引用到当前对象的 this,以方便对属性赋值以及调用对象内部方法,返回整个对象 |
also | it | 当前对象 | 是 | 打印日志,不返回最后一行的值,而是返回整个对象,其余与 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作用域函数的主要内容,如果未能解决你的问题,请参考以下文章