一些你可能不知道的神秘 Kotlin 语法糖
-
一些奇怪的语法糖
这些语法糖在
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() } }
-
好糖 还有吗
-
学到了
-
@diyigemt 还有一些糖等有时间了再补上x
-
nb大佬 我的评价是gkd
-
更新了一下,写得有点水,下午接着写。
晚上再写几个语法糖, 挖个坑先。import net.mamoe.mirai.event.events.BotMuteEvent fun test(run: (BotMuteEvent) -> Unit) { } fun main() { test { (durationSecond,operator) -> // For data class } }
和
class WrongYamlTypeError(vararg val shouldBe: String) {}
-
更新了关于重载操作符操作的说明,总字节 7583