Kotin 语法糖

739 阅读4分钟

前言

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。

  • 参数

it.png

this.png

其次从 lambda 表达式的参数可以看出,it 和 also 都是 it ,剩下的 run/with/apply 都是 this 。其实 run 和 with 是的表现是完全一致的,只是调用方式不同而已,run 只需要一个参数,而 with 需要把接受者和 lambda 同时传入。

类型参数返回值
letitlambda 表达式最后一行,默认为 kotlin.Unit
alsoit接受者,即调用方法的对象
applythis接受者,即调用方法的对象
withthislambda 表达式最后一行,默认为 kotlin.Unit
runthislambda 表达式最后一行,默认为 kotlin.Unit
原理剖析

总的来说,这几个内置函数的实现是高度相似的,都是使用了 Kotlin 高阶函数的特性。但是他又是如何实现这些微妙的差异的那?我们可以对比一下 letalso

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 呢? 可以对比一下 alsoapply

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 这几个常用的内置函数,就高阶函数的定义做了最好的师范。