val 和 const

在 Kotlin 中 val 修饰的变量并不是常量,而是“只读变量”。所谓只读,指的是它只能被 赋值一次,但并不保证它的值在运行时不可变。

在 Kotlin 中,一个类的成员变量通常由 backing field + getter + setter 组成:

  • var:有 getter 和 setter。

  • val:只有 getter,没有 setter。

因此,val 修饰的属性仍然可能在每次访问时返回不同的值。例如:

class Simple {
    val x: Int
        get() {
            return Random.nextInt(10)
        }
}

fun main() {
    val s = Simple()
    println(s.x) // 第一次调用
    println(s.x) // 第二次调用
}

运行结果中,s.x 的两次输出可能不同。这说明 val 并非真正的常量,只是外部无法直接对其重新赋值。

如果需要定义真正意义上的常量(类似 Java 中的 final static),就必须使用 const 关键字。
const 修饰的属性必须在 编译期 就确定值,因此它只能修饰 顶层属性object 单例对象中的属性,或 companion object 中的属性。

// 顶层属性
const val TAG = "666" 

// 在 object 中使用
object SimpleObj {
    const val X = 20
}

class Simple {
    // 在伴生 object 中使用
    companion object {
        const val NUM = 10
    }
}

总结:

  • val:只读变量,运行期可变,但对外只读。

  • const:编译期常量,必须在编译期确定值,只能修饰顶层/object/companion object 的属性。

高阶函数

高阶函数是指:可以接收函数作为参数,或者返回一个函数作为结果的函数。在 Kotlin 中,函数可以像对象一样传递、存储和返回。高阶函数的强大之处在于它们常常与 Lambda 表达式 一起使用,使得代码更加简洁。

常见场景:

  • 函数式编程风格:集合操作 list.map{ }

  • 回调函数(Callback):button.setOnClickListener { println("clicked") }

fun operate(x: Int, y: Int, op: (Int, Int) -> Int): Int {
    return op(x, y)
}

fun main() {
    val sum = operate(3, 5) { a, b -> a + b }
    println(sum) // 输出 8
}

在 JVM 上,Kotlin 的高阶函数其实是通过 函数类型对象 来实现的。

  • 在 Kotlin 中 (Int) -> String 这种函数类型,本质上是编译成 kotlin.jvm.functions.FunctionN 接口 的匿名类。

  • FunctionN 是一组接口,比如:

    • Function0<R>:无参函数

    • Function1<P1, R>:一个参数

    • Function2<P1, P2, R>:两个参数

    • 以此类推,最多支持 Function22

上面代码中的 opreate() 函数编译后的内容大致如下,函数参数 op 实际上是 Function2 接口的实例。

public static final int operate(int x, int y, @NotNull Function2 op) {
    Intrinsics.checkNotNullParameter(op, "op");
    return ((Number) op.invoke(x, y)).intValue();
}

Kotlin 的高阶函数是通过 函数类型 编译成 FunctionN 接口的实现来完成的,Lambda 会被编译成实现了 FunctionN 接口的匿名类对象。如果 Lambda 捕获了外部变量,就会生成一个闭包类。为了避免性能开销,Kotlin 提供了 inline,在编译期直接内联代码,从而减少对象分配和调用开销。

内联函数

在 Kotlin 中,可以使用 inline 关键字将函数声明为内联函数。所谓内联,是指在 编译期 编译器会进行“代码展开”,具体包括两方面:

Kotlin 中使用 inline 关键字修饰的函数是内联函数。在编译时会将:

  • 函数本身的调用会被内联到调用处,避免函数调用的栈开销;

  • 函数参数(尤其是 Lambda 表达式)也会被内联到调用处,避免生成额外对象,从而提升性能。

例如,Kotlin 标准库中的 forEach 就是一个典型的内联函数:

public inline fun IntArray.forEach(action: (Int) -> Unit): Unit {
    for (element in this) action(element)
}

当调用 forEach 时,编译器会直接将传入的 Lambda 代码“拷贝”到循环体中,而不是生成额外的方法调用。因此,内联在高阶函数(接收 Lambda 参数的函数)中尤为常用。

内联高阶函数的 return

在内联高阶函数中,return 的行为有一些特殊之处。比如在 forEach 中,无法实现 break 的效果(直接跳出整个循环),只能实现 continue 的效果(跳过当前元素,继续下一次循环)。

val a = intArrayOf(1, 2, 3, 4)
a.forEach {
    if (it == 3) {
        return@forEach  // 局部返回,相当于 continue
    }
    println(it)
}

等价于传统的循环写法:

for (element in a){
    if (element == 3){
        continue
    }
    println(element)
}

这里的 return@forEach 被称为 标签返回(labeled return),它只跳过当前这次 Lambda 的执行,而不会终止整个外层函数。

拓展函数

Kotlin 的扩展函数(Extension Function)是一种在 不修改类源码、不继承该类 的情况下,为现有类“添加”新函数的机制。

fun String.lastChar(): Char {
    return this[this.length - 1]
}

fun main() {
    println("Kotlin".lastChar()) // 输出 'n'
}

扩展函数并没有真正修改类本身,它在 编译期 被静态解析成 静态方法调用,实例本身作为第一个参数传入方法。所以扩展函数只是语法糖,不是真正的类方法。编译后的 Java 代码大致是:

public static final char lastChar(@NotNull String $this$lastChar) {
    Intrinsics.checkNotNullParameter($this$lastChar, "<this>");
    return $this$lastChar.charAt($this$lastChar.length() - 1);
}

注意:拓展函数可以和成员函数同名,如果签名相同,成员函数优先

协程

suspend 函数是如何挂起的?

Kotlin 的 suspend 函数挂起是通过 Continuation 机制 + 状态机改写实现的。编译器会把 suspend 函数编译成接收 Continuation 的函数,每次遇到挂起点时,保存当前执行状态并返回。等到结果准备好时,通过 Continuation.resume() 恢复协程,继续从挂起点往下执行。它不会阻塞线程,只是保存/恢复执行状态。