在进行 Kotlin DSL 开发时,用到了 Kotlin DSL 的 逻辑运算符(and,or)等,本来以为 Kotlin DSL 中逻辑运算符在短路策略中,和 Java 及 普通 Kotlin 代码一致,但是发现效果并不如此。本文将介绍 Kotlin 中逻辑运算符进行简单介绍,并简单剖析实现机制。本文将主要采用代码示例分析。
1. 问题引入
1.1 Kotlin 中逻辑运算符 &&处理
我们首先看下面的代码:
data class Person(val name: String, val friends: List<Person> = listOf())
fun main() {
val mike = Person("mike")
val john = Person("john", listOf(mike))
val lili = Person("lili", listOf(mike, john))
println(lili.friends.isNotEmpty() && lili.friends[0].name == "mike")
println(mike.friends.isNotEmpty() && mike.friends[0].name == "john")
}
复制代码
如代码中所示:
mike 只有一个名字,并没有一个朋友,friends 是一个空的 List。虽然如此,在第 9 行,进行判断时,虽然我们有 mike.friends[0].name
中,取 mike 的第一个朋友的操作,但是因为 短路操作 的处理,前面 mike.friends.isNotEmpty()
已经返回了 false,所以,不会执行到后面的语句,后面的语句是安全的。
上面的代码运行,会有如下输出:
1.2 Kotlin 中逻辑运算符 and 处理
但是,如果我们考虑 Kotlin DSL 操作,则会有不同的现象。看下面的代码:
package zmj.test.kotlin.dsl
data class Person(val name: String, val friends: List<Person> = listOf())
infix fun Person.match(block: Person.() -> Boolean): Boolean {
return block(this)
}
fun main() {
val mike = Person("mike")
val john = Person("john", listOf(mike))
val lili = Person("lili", listOf(mike, john))
println(lili match { (friends.isNotEmpty()) and (friends[0].name == "mike") })
println(mike match { (friends.isNotEmpty()) and (friends[0].name == "john") })
}
复制代码
如上代码,我们在第 3-5 行,定义了一个 中缀函数,对 Person 进行校验,看 Person 中字段是否能够满足特定条件。
此时在第 13 行,我们的写法还是和 1.1 中示例一样,首先对 friends 进行校验,如果非空,再校验 friends 的第一个元素的名字是否为 john。
但是,此时执行代码,运行结果如下:
true
Exception in thread "main" java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 0.
at kotlin.collections.EmptyList.get(Collections.kt:36)
at kotlin.collections.EmptyList.get(Collections.kt:24)
at zmj.test.kotlin.dsl.MainKt$main$2.invoke(main.kt:15)
at zmj.test.kotlin.dsl.MainKt$main$2.invoke(main.kt)
at zmj.test.kotlin.dsl.MainKt.match(main.kt:6)
at zmj.test.kotlin.dsl.MainKt.main(main.kt:15)
at zmj.test.kotlin.dsl.MainKt.main(main.kt)
Process finished with exit code 1
复制代码
如上,有个异常 IndexOutOfBoundsException,在代码第 15 行。但是,我们在取 friends 的第一个 元素之前,有判空操作,为什么还是会有下标越界异常呢?
1.3 and 和 && 区别
其实,最开始,我以为是 Kotlin 中,普通的操作 和 DSL 操作有区别,因为,我们习惯使用 and 和 or 作为 DSL 操作,更符合自然语言特征,而 && 和 || 更像是代码的操作符。不过根据 Kotlin 代码注释,发现有很大区别,如下:
/**
* Performs a logical `and` operation between this Boolean and the [other] one. Unlike the `&&` operator,
* this function does not perform short-circuit evaluation. Both `this` and [other] will always be evaluated.
*/
public infix fun and(other: Boolean): Boolean
/**
* Performs a logical `or` operation between this Boolean and the [other] one. Unlike the `||` operator,
* this function does not perform short-circuit evaluation. Both `this` and [other] will always be evaluated.
*/
public infix fun or(other: Boolean): Boolean
复制代码
上面代码,是 Kotlin.Boolean 中抠出来的。上面提示:and 是逻辑与操作,但是,and 并不会执行短路操作,而是会进行全部比较操作。因此,and 和 &&, or 和 ||,并不仅仅 Kotlin 的一种 alias 的操作符,还是有很大区别的。
那么,and 和 or 是如何实现的呢?下一部分还是以 and 和 && 进行分析。
2. Kotlin 逻辑运算符 and 实现机制
2.1 Kotlin 逻辑运算符 and 编译结果分析
我们都知道,Kotlin 的代码,都是编译成 Java 的字节码,然后在 Java 虚拟机中运行的。因此,分析 Kotlin and 操作的处理,从 Kotlin 代码编译后的字节码上面分析非常恰当。
上面的 Kotlin 代码,生成的 class 文件有四个:
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/10/4 21:59 1767 MainKt$main$1.class
-a---- 2021/10/4 21:59 1767 MainKt$main$2.class
-a---- 2021/10/4 21:59 2235 MainKt.class
-a---- 2021/10/4 21:59 3241 Person.class
复制代码
我们反编译代码(我使用的是 jd-gui 工具),然后查找对应于上面第 15 行的反编译的代码信息,内容如下:
public final boolean invoke(@NotNull Person $receiver) {
Intrinsics.checkNotNullParameter($receiver, "$receiver");
List<Person> list = $receiver.getFriends();
boolean bool = false;
return (!list.isEmpty()) & Intrinsics.areEqual(((Person)$receiver.getFriends().get(0)).getName(), "john");
}
复制代码
从上面反汇编出来的结果,Kotlin 中的 and 运算,最终处理后的操作是 Java 中的 & 操作。
2.2 Java 中 &&和 &的区别和联系
(1) 相同点:
“&&”和 “&”都可以用作逻辑与的运算符 ,当运算符两边的表达式的结果都为 true 时,整个运算结果才为 true,否则,只要有一方为 false,则结果为 false。
(2) 不同点:
3. 总结
本文介绍分析了 Kotlin 中,and 和 &&,or 和||等,不同的操作符的区别和联系,并剖析了 and 的实现方式。
评论