一些奇怪的语法糖
这些语法糖在 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重载运算符 视为一个方法
当运算符位于一个类伴生对象中时,你可以直接从类调用运算符
例如:
此类运算符并不要求提供参数
运算符 对应函数名称 + unaryPlus() - unaryMinus() ! not()这些函数并不要求返回类型,你可以不返回任何内容.
递增 & 递减此类运算符要求不提供参数
此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
它们不应改变调用的对象。
注: 前缀与后缀都属于此类操作 a++和++a都调用的是同一个函数
运算符 对应函数名称 ++ inc() -- dec() 二元运算符此类运算符要求提供参数
以下运算符中,要求提供一个参数
运算符 对应函数名称 + plus - minus * times / div % rem .. rangeToKotlin 1.9.0 新增运算符
..< rangeUntil以下运算符在实际运算符中会为变量重新赋值,需要一个参数
此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
它们不应改变调用的对象。
以下运算符需要传递一个参数,并且必须返回布尔值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 委托属性, 有 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
这可能会降低你的代码可读性,请酌情使用.
本地函数 Local Function以此类推,我们还有
operator fun <T> List<T>?.get(index: Int) = this!![index]功能同上,可直接调用 索引运算符
此处的本地函数不指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 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代码
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内联函数中的内容都会运行。
这相当于
理论上,你可以不断||下去,只要最右侧跳出即可
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
在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 handleData() {} fun main() = suspend { handleData() }.runBlock() fun (suspend () -> Any).runBlock() { runBlocking { invoke() } }