一些奇怪的语法糖
这些语法糖在 Kotlin 1.7.10 的环境中都可以运行
具体这些语法糖是在什么时候更新的,还请各位读者自己私下去探索
本文仅是分析一些笔者在实际项目开发中遇到的神奇的糖,在此进行分享。
你可以看作本文为抛砖引玉中的砖,可以在本文下进行更多奇怪糖分的讨论。
不稳定更新中x
重载运算符
对应 Kotlin 文档 Kotlin Docs Operator overloading
在一些常见的语言中,你可能会看到一些支持重载运算符的的语言,如:Rust,C#,C++等众多语言中都支持这一操作
在编译期间,这些操作符一般都会编译为调用对应的函数
例如,在kotlin中
val list = listOf(0,1,2,3,4)
val element = list[0]
这段代码实则等同于
val list = listOf(0,1,2,3,4)
val element = list.get(0)
其中的 [0] 被编译为了调用函数 get(0)
因此,调用运算符其实本质上属于调用函数
那我们可否实现像实现函数一样实现重载运算符呢?
答案是可以的,让我们以mirai中MessageChain的部分代码为例:
operator fun get(key: MessageKey<SingleMessage>): SingleMessage? {
return firstOrNull { /* ... */ true } as SingleMessage?
}
为便于理解,删除了注解, 部分可省略的声明以及泛型声明
这段代码的作用是从 MessageChain 中获取支持的元素源
举个例子,从 MessageChain 中获取所包含的图片信息源
val message: MessageChain = ...;
val imageSource = message[Image.Key]
在这段代码中,我们可以使用 [] 运算符
operator fun 是一种标识符,它代表着这个函数会重载运算符
(当为 覆盖override 时,可省略operator关键词)
在操作符中,存在有
一元运算符
重载函数不需要参数
例如我们常见的 递增 递减 等操作
一般来说,重载运算符不应改变调用的对象
二元运算符
重载函数需要提供参数
参数的数量不限, 甚至支持 vararg 选项
一些特殊的运算符
例如对于属性的委托(by) 也可以使用运算符实现
以及如 迭代器 等 操作(并不进行阐述,因为没什么特殊的)都可以运算符重载
重载操作符操作并不限制代码所在模块
你可以使用 扩展函数 以扩展指定对象的运算符
Kotlin 要求重载运算符为成员函数*(在一个对象中,包含接口)*或扩展函数
运用重载运算符可以实现许多神奇的功能
可惜的是,在Java中并不存在此类操作,所有在 kotlin 中的重载运算符在 Java 中并没有效果
Java 会将 Kotlin重载运算符 视为一个方法
当运算符位于一个类伴生对象中时,你可以直接从类调用运算符
例如:
class Example {
companion object{
operator fun get(int: Int) {
TODO()
//...
}
}
}
fun testExample() {
Example[114514] // 可通过编译
}
一元前缀运算符
此类运算符并不要求提供参数
运算符
对应函数名称
+
unaryPlus()
-
unaryMinus()
!
not()
这些函数并不要求返回类型,你可以不返回任何内容.
递增 & 递减
此类运算符要求不提供参数
此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
它们不应改变调用的对象。
注: 前缀与后缀都属于此类操作 a++和++a都调用的是同一个函数
运算符
对应函数名称
++
inc()
--
dec()
二元运算符
此类运算符要求提供参数
以下运算符中,要求提供一个参数
运算符
对应函数名称
+
plus
-
minus
*
times
/
div
%
rem
..
rangeTo
Kotlin 1.9.0 新增运算符
..<
rangeUntil
以下运算符在实际运算符中会为变量重新赋值,需要一个参数
此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
它们不应改变调用的对象。
运算符
对应函数名称
+=
plusAssign
-=
minusAssign
*=
timesAssign
/=
divAssign
&=
remAssign
以下运算符需要传递一个参数,并且必须返回布尔值Boolean
运算符
对应函数名称
in
contains
==
equals
!in 实则等同于 !contains
在 == 中
针对null 此操作永远不会调用equals, 始终为true
一些特殊的运算符
常有的 [] 索引访问运算符, ()调用运算符, 甚至是 by 都可以通过此重载
索引访问运算符 []
对应函数名: get或set
get 用于访问元素, set 用于修改元素
不限参数长度, 但至少有一个参数。 不限返回类型.
对照表:
运算符
对应函数
a[i]
a.get(i)
a[i,j]
a.get(i,j)
a[i] = b
a.set(i,b)
a[i, j] = b
a.set(i, j, b)
在 set 函数中
修改后的元素始终在参数的最后一位
调用运算符 ()
对应函数名 invoke
实现后,它看起来就像是在实例化一个对象一样.
对于 lambda , 这通常被用来调用 lambda
例如:
fun example(lambda: String.(Int) -> Unit) {
// 我们可以通过 () 调用代替 invoke
lambda("String", 114514) // 看起来和调用函数一样
}
但当 invoke 处于一个类的伴生对象时,可能会导致 构造函数 与 invoke 函数重合的状况
在这种情况下 kotlin 会优先考虑是否为构造函数,随后被编译为实例化对象
class TestOperator{ // 默认空构造参数
init {
println("--init--")
}
companion object {
operator fun invoke() {
println("--operator invoke--")
}
}
}
fun main() {
TestOperator()
}
在这段代码中,就会得到输出 --init--
而当运算符未一致时,如:
class TestOperator{ // 默认空构造参数
init {
println("--init--")
}
companion object {
operator fun invoke(int: Int) {
println("--operator invoke $int--")
}
}
}
fun main() {
TestOperator(114514)
}
这时候的输出就是 --operator invoke 114514-- 了,因为它被编译为了调用重载运算符(不符合构造函数)
比较运算符
此处逻辑与 Java 一致
函数名为 compareTo 传入一个参数,始终返回一个Int类型的数据
一般来说,
当此对象小于另一个对象时, compareTo 返回小于0的数据(一般为-1)
当此对象值等于另一个对象时, compareTo 返回 0
当此对象大于另一个对象时, compareTo 返回大于0的数据(一般为1)
运算符
对应函数名称
a > b
a.compareTo(b) > 0
a < b
a.compareTo(b) < 0
a >= b
a.compareTo(b) >= 0
a <= b
a.compareTo(b) <= 0
委托属性运算符
在 Kotlin 中存在有委托字段的操作
例如:
val finalDelegatedProperty by lazy { } // `lazy {}` 为委托器实现
var mutableDelegateProperty by lazy { }
通过 重载运算符 你可以实现委托字段的操作
对于 val 委托属性, 有 getValue(thisRef: Any?, property: KProperty<*>):字段类型
对于 var 委托属性, 有 getValue(thisRef: Any?, property: KProperty<*>):字段类型, 以及 setValue(thisRef: Any?, property: KProperty<*>, value: 字段类型)
thisRef 为属性需要的对象, 当为成员属性时为所属对象 反之则为null
property 是为当前属性的相关信息,如属性名称等
*在 Kotlin 中实现切片
这是一段在rust中的字符串切片实现
fn slice() {
let mut string = String::from("Hello World!");
let slice = &string[0..3]; // 值为: Hell
}
Rust 中的切片还支持有如..y, .., x.. 的实现 但这些都难以在 kotlin 中实现,故不参与讨论
在kotlin中,我们有着针对 x..y 的操作符实现 (range-to 操作符)
所以, 0..3这段代码实则等于 IntRange(0,3)
我们只需要一个接受 IntRange 的函数即可
由于在 String 中并没有定义 get(IntRange) 这一函数, 所以在这里我们需要通过扩展函数以实现功能
operator fun String.get(range: IntRange): String {
return this.substring(range) // 截取字符串指定范围
}
这样,我们就实现了字符串切片的内容。同样地,我们还可以实现集合切片:
operator fun <T> List<T>.get(range: IntRange): List<T> {
return this.subList(range.first,range.last)
}
这样,我们就可以在 kotlin 中运行以下代码了
fun slice() {
val str = "Hello World!"
val strSlice = str[0..3] // 值为 Hell
val list = listOf(0,1,2,3,4)
val listSlice = list[0..3] // 值为 0,1,2,3
}
operator fun String.get(range: IntRange): String {
return this.substring(range)
}
operator fun <T> List<T>.get(range: IntRange): List<T> {
return this.subList(range.first,range.last)
}
*可 null 值 的扩展函数
或许你会有这样的需求
一个Map中有多个嵌套Map,
一般来说,Map通过get函数获取对应的值时,会返回一个可null值 V?
此时我们尝试获取最后一个嵌套 Map 内的值,就会显得十分不优雅
val example =
mutableMapOf<String, Map<String, Map<String, Map<String,Long>>>>()
// 真的会有人这样写吗?
fun getFinalValue() {
val finalValue: Long =
example["Key 0"]!!["Key 1"]!!["Key 2"]!!["Key 3"]
// Or ...
val finalValue1: Long =
example["Key 0"]?.get("Key 1")?.get("Key 2")?.get("Key 3")
}
由于 Kotlin 中的运算符仅支持对于 非null值的调用
所以,
要么,我们需要使用一连串的!!以将值转换为
不可空值(无法为null)
要么,我们使用?.调用方法,而一连串的?.get(..)则显得更加不美观
此时,我们可能使用造轮传统来防止这样的清空发生
同样地,我们依旧需要使用扩展函数以支持从外界重写操作符
operator fun <K,V> Map<K,V>?.get(key: K): V {
return this!![key] // Or more...
}
在这里,我们通过 Map<K,V>? 使它支持了对于可空值的[]语法支持
让我们尝试用这段代码代替原本的代码吧!
operator fun <K,V> Map<K,V>?.get(key: K): V {
return this!![key]
}
val example =
mutableMapOf<String, Map<String, Map<String, Map<String,Long>>>>()
fun getFinalValue() {
val finalValue = example["Key 0"]["Key 1"]["Key 2"]["Key 3"]
}
这样,我们就成功使代码看着更加美观。
不过,这样写具有一定的坏处———你无法从代码层次直接知晓这个值是否可能为null
这可能会降低你的代码可读性,请酌情使用.
以此类推,我们还有
operator fun <T> List<T>?.get(index: Int) = this!![index]
功能同上,可直接调用 索引运算符
本地函数 Local Function
此处的本地函数不指native函数,而是指函数嵌套内的函数
不建议在实际生产中这么做,它会大大降低你的代码可读性
总所周知,在Kotlin中,我们可以通过fun关键词声明一个函数
它看起来会是这样
fun helloWorld() {
println("Hello World!")
}
然而你知道吗,我们还可以往一个函数中套更多的函数
像这样
fun helloWorld() {
fun getHelloWorldMessage() = "Hello World"
println(getHelloWorldMessage())
}
我们可以发现,这段函数竟然被成功编译,并且成功运行输出Hello World
让我们用idea自带的显示字节码,查看相关代码
// class version 55.0 (55)
// access flags 0x31
// 普通函数 helloWorld: Unit
public final static helloWorld()V
L0 // 调用内嵌函数 getHelloWorldMessage()
LINENUMBER 5 L0
INVOKESTATIC com/pigeonyuze/DeliciousSugarKt.helloWorld$getHelloWorldMessage ()Ljava/lang/String;
POP
// More code...
// helloWorld 嵌套本地函数 getHelloWorldMessage: String
private final static helloWorld$getHelloWorldMessage()Ljava/lang/String;
L0
LINENUMBER 4 L0
LDC "HelloWorld"
ARETURN
L1
MAXSTACK = 1
MAXLOCALS = 0
MAXLOCALS = 0
由上可见,我们的代码实则被编译成了两个方法
helloWorld() 和 helloWorld$getHelloWorldMessage() 方法
而其中本地函数的标识符无论如何为private
Note: 你始终无法为嵌套函数设置标识符,你始终只能从本地函数所在函数中调用它
如:
fun superFunction() {
fun childFunction() {}
childFunction() // 可以通过编译
private fun childFunction1() {}
// 无法通过编译: Modifier 'private' is not applicable to 'local function'
// NOTE: 无法向本地函数添加标识符
}
fun elseFunction() {
childFunction()
// 无法通过编译: Unresolved reference : getHelloWorldMessage
// NOTE: 无法从外处调用本地函数
}
或许你可以猜到,你可以无限地嵌套函数
fun helloWorld() {
fun helloWorld0() {
fun helloWorld1() {
//More shit
}
}
}
这些函数经过编译后,都会被编译为一个单独的 private final函数。
值得注意的是,kotlin中的inline函数并不支持本地函数的使用。
kotlin的inline函数会将原函数体内联到调用方处,因此如果内联了本地函数可能会导致意想不到的错误。
逻辑或|| Logical OR
我们大家都知道, 我们可以通过逻辑或门||获取一串布尔值中是否包含真值(true)
false || true // true
true || true // true
false || false //false
它是按照从左到右的顺序来计算的。
如果你有着乱翻kotlin代码的习惯的话
你会在kotlin/native关于Throwable获取栈信息的源码里看到以下代码
// kotlin.Throwable.kt line 77
private fun Throwable.dumpFullTrace(indent: String, qualifier: String) {
this.dumpSelfTrace(indent, qualifier) || return
var cause = this.cause
while (cause != null) {
cause.dumpSelfTrace(indent, "Caused by: ")
cause = cause.cause
}
}
一句神奇的代码躲藏在其中
this.dumpSelfTrace(indent, qualifier) || return
是的没错,这是一个逻辑或门,可是它有着这么一个与众不同的点
运算符的左侧是布尔值,而右侧却是函数跳出点
通过反编译为java代码
if(!this.dumpSelfTrace(indent, qualifier)) {
return;
}else {
Throwable cause = this.cause;
while (cause != null) {
cause.dumpSelfTrace(indent, "Caused by: ");
cause = cause.cause;
}
}
Kotlin 编译器将这段||的神奇代码,转变成了if-else。
当逻辑或运算符左侧不满足时,理应向右继续计算,而此时的右值为return,函数的跳出点。
那么此时,kotlin该做什么呢?没错,它执行了右侧的语句return代码,这使得程序直接跳出。
此时,我们甚至可以执行run函数(需要为inline函数,因为这样可以跳出调用处函数),只要它跳出了函数就行
fun getBoolean(): Boolean { /* More code */ }
fun call() {
getBoolean() || run {
println("Wow, I'm running!")
throw Throwable()
// Also, you can just 'return'
}
println("Go go go!")
/* More code... */
}
调用call()后,如果getBoolean不满足则会运行run内联函数,因为内联函数中最后会跳出函数(抛出错误或return函数),run内联函数中的内容都会运行。
这相当于
fun getBoolean(): Boolean { /* More code */ }
fun call() {
if(!getBoolean()) {
println("Wow, I'm running!")
throw Throwable() //Also, you can just 'return'
}
println("Go go go!")
/* More code... */
}
理论上,你可以不断||下去,只要最右侧跳出即可
Random.nextBoolean() || (Random.nextBoolean() && Random.nextBoolean()) || /* More Boolean*/ || return
这依托代码你是可以成功编译并运行的,他将会像上文一样编译为一段if-else,就像:
if (!Random.nextBoolean()) {
if(!(Random.nextBoolean() && Random.nextBoolean())) {
if(!/*More Boolean*/) return
}
}
在循环体中,你还可以使用break或continue
while(true) {
booleanValue() || continue
booleanValue() || break
booleanValue() || return
booleanValue() || throw Throwable()
}
这些代码同样也可以编译或运行
是否在实际项目中使用可根据实际项目复杂度来看
毕竟过度的||,何尝不是依托呢(笑
需要 Lambda 的函数
在讨论如何编写关于 lambda 的函数前,或许我们可以先来了解一下什么是lambda
在kotlin中,几乎所有的表达式中的{}都会被编译为一个lambda对象
如果你学习或使用过java,会发现这一点与java亦或是其他语言都有所不同。
你可以猜想运行以下代码,你会得到哪些输出.
fun main() {
{ println("Hello world") }
if (true) { println("I am in an if expression") }
when {
true -> { println("I am in an when expression") }
}
{ println("Invoked lambda by call invoke function") }.invoke();
{
println("Invoked lambda by this.()")
}()
val lambda = {
println("Um-mm. why i am in a local variable.")
}
lambda() // = lambda.invoke()
val newLambda = { "Please invoke me." } // UNUSED
run {
println("Run!")
}
}
运行以上代码,我们得到了以下输出
I am in an if expression
I am in an when expression
Invoked lambda by call invoke function
Invoked lambda by this.()
Um-mm. why i am in a local variable
Run!
等等,是不是少了一句"应有的"输出。
或许我该告诉你,kotlin的{}就是这么反直觉。
{ println("Hello world") }
这段代码,被解析为了一个 lambda 表达式,而从未调用的 lambda 被编译器所忽略
或许并没有忽略,在字节码中,这段代码看起来是这样的
LINENUMBER 2 L0
GETSTATIC ...
POP
在字节码层面,它执行了压栈和出栈操作,但仅此而已。
if (true) { println("I am in an if expression") }
when {
true -> { println("I am in an when expression") }
}
而这两段代码中的{}并没编译为 lambda 表达式,故可以直接运行。
lambda 表达式并不会解析为一个单独的 函数/方法
要想调用 lambda 表达式,你应该通过 invoke 函数进行调用
或者你可以使用 kotlin.run 这一个内联函数
public inline fun <R> run(block: () -> R): R {
/* more codes */
return block()
}
这是一个需要传入 lambda 的函数
我们来看看它的参数声明部分
block: () -> R
block 为参数名
() -> R 为参数类型
让我们从 () -> R 中谈谈.
-> 表明了这是一个 lambda 体
-> 后的内容(R)表明了这一个lambda体需要返回的类型
-> 前的内容(())表明了这一个lambda体需要提供的类型
以此类推,我们现在由以下表达式
String.(Int) -> Any
如果你知道什么是扩展函数,这里会更好理解一些
fun String.lambda(it: Int): Any
String 是接收器,
在 lambda 体中使用 this 调用
Int 是形参,
在 lambda 体中通常使用 it 调用,也可以使用形参声明规定调用名称, 如`{ int -> /**/ }
Any 是返回类型
你也可以使用泛型,像是这样
K.(V) -> R
写在函数参数里面,它看起来像是这样:
fun <K,V,R> invoke(name: K.(V) -> R) {} // 使用 `name` 调用 lambda
或者也可以作为扩展函数,像这样:
fun <K,V,R> (K.(V) -> R).invoke() {} // 使用 `this` 调用 lambda
你甚至可以使用 suspend 标识这个 lambda 可以为挂起的。例如:
suspend fun (suspend () -> Any).run() {
this.invoke()
}
suspend fun run(arg: suspend () -> Any) {
this.invoke()
}
总结一下,如果你需要将 lambda 作为函数的 形参/扩展函数接收器
你可以这么写:
suspend fun <K,V,R> (suspend K.(V) -> R).run0() {}
suspend fun <K,V,R> (suspend K.(V) -> R).run1(run: suspend K.(V) -> R) {}
fun <K,V,R> (K.(V) -> R).run2() {}
// ...
fun <K,R> (K.() -> R).run3() {}
fun <R> (() -> R).run4() {}
fun <R> run5(arg: () -> R) {}
实际生产可参考案例
suspend fun handleData() {}
fun main() =
suspend {
handleData()
}.runBlock()
fun (suspend () -> Any).runBlock() {
runBlocking { invoke() }
}