前言
Kotlin 语法糖的总结和原理分析。
Kotlin 有很多实用的语法糖,比如扩展函数、object 单例、apply/run/with 等内置函数,对于开发者来说非常的友好的方便。简单梳理和总结包括但不限于上述这些语法糖的内容。
Syntactic Sugar
内置函数
kotlin-stdlib 内的 Standard.kt 文件内定义了几个比较实用的顶层函数 比如 apply/with/run/let/also
等,这几个函数的功能比较相似,但又略微有些差异,在此梳理一下。
- 示例
fun main() {
val sugar = Sugar("mike", 21, true)
printInfo(sugar)
val letResult = sugar.let {
it.name = "let"
it.age = 9
}
printInfo(letResult)
val alsoResult = sugar.also {
it.name = "also"
it.age = 13
}
printInfo(alsoResult)
val withResult = with(sugar) {
name = "with"
age = 10
}
printInfo(withResult)
val runResult = sugar.run {
name = "run"
age = 11
}
printInfo(runResult)
val applyResult = sugar.apply {
name = "apply"
age = 12
}
printInfo(applyResult)
}
output
- 返回值
Sugar(name=mike, age=21, happy=true) : com.ext.Sugar
kotlin.Unit : kotlin.Unit // let
Sugar(name=also, age=13, happy=true) : com.ext.Sugar // also
kotlin.Unit : kotlin.Unit // with
kotlin.Unit : kotlin.Unit // run
Sugar(name=apply, age=12, happy=true) : com.ext.Sugar // apply
首先从返回结果,可以看到,默认情况下 apply 和 also 返回的都是当前对象,let/with/run 返回的是 kotlin.Unit ,也就是在 Lamdba 表达式中如果没有显示的在最后一行写返回值,那么 kotlin.Unit 就是返回值,可以理解为 Java 中的 Void。
- 参数
其次从 lambda 表达式的参数可以看出,it 和 also 都是 it ,剩下的 run/with/apply 都是 this 。其实 run 和 with 是的表现是完全一致的,只是调用方式不同而已,run 只需要一个参数,而 with 需要把接受者和 lambda 同时传入。
类型 | 参数 | 返回值 |
---|---|---|
let | it | lambda 表达式最后一行,默认为 kotlin.Unit |
also | it | 接受者,即调用方法的对象 |
apply | this | 接受者,即调用方法的对象 |
with | this | lambda 表达式最后一行,默认为 kotlin.Unit |
run | this | lambda 表达式最后一行,默认为 kotlin.Unit |
原理剖析
总的来说,这几个内置函数的实现是高度相似的,都是使用了 Kotlin 高阶函数的特性。但是他又是如何实现这些微妙的差异的那?我们可以对比一下 let
和 also
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
- 可以看到
block: (T) -> R
block 函数的参数类型就是 T,也就是调用者。因此 lambda 表达式的参数名称就是it
- 再看返回值
let
直接返回了 block 函数的运行结果,而这个 block 函数就是我们调用时传入的 lambda 表达式,因此其执行结果就是整个函数的结果。而also
block 函数时返回值就是 Unit ,也就是说 lambda 表达式的结果是被忽略的。这里可以认为调用 block 只是为了执行一项操作,而实际返回是this
再来看看为什么有时候参数是 it ,有时候又是 this 呢? 可以对比一下 also
和 apply
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
- 这里的关键就是 block 函数的定义。 注意到 apply 中
block T.() -> Unit
的写法,可以看到这里明确了当前函数执行的类型,同时参数为空;可以试一下,这种情况下,定义参数是没有意义的。
public fun <T> T.apply1(block: T.(Int) -> Unit): T {
block(1)
return this
}
比如这里,虽然定义了 block 的参数为 Int 类型,但是因为应明确定义了 block 函数是在 T 类型执行,因此实际调用时也无法传递这个参数,因此这里实现时也无法获取到具体的参数值 。
小结
Kotlin 高阶函数是平日开发中最常用的功能,使用高阶函数可以实现代码逻辑的简化和封装,最重要的一点就是把函数当参数的特性,让方法的行为能够被另外一个方法的行为控制,甚至是实现套娃。一些比较常见的三方库比如 LeakCanary/OkHttp 等使用 Kotlin 重写之后也是大量使用了高阶函数。而 let/also/apply/run/with 这几个常用的内置函数,就高阶函数的定义做了最好的师范。