Kotlin高阶探索-初始化顺序、inline、crossinline、reified、枚举、委托、Nothing

187 阅读17分钟

本文主要介绍 Kotlin 中的一些初始化顺序、函数内联、内联相关关键字(noinline、crossinline、reified)、枚举、可空类型以及委托机制等内容。


一、Kotlin 的初始化顺序

在 Kotlin 中,不论是顶层类还是继承层次里的「子类」,它自己的属性初始化表达式和 init {} 代码块,都会在 该类的主构造函数 完成期间执行完毕,之后才轮到它的次级构造函数体。先给一张总顺序表,再用代码验证。

┌── 主构造阶段 ───────────────┐   ┌─ 次级构造函数体 ─┐
① 主构造参数默认值
② 所有属性初始化表达式      <-------都在这里一次性完成
③ 所有 init { } 代码块
└───────────────┬───────────┘
                │—— this(...) 委托调用
┌───────────────▼───────────┐
④ 次级构造函数代码块         ← 只有这一步属于“次构造体”
└───────────────────────────┘

用打印验证执行顺序

class Demo(val p: Int = log("param")) {       // ①
    val a = log("prop a")                     // ②
    val b = log("prop b")                     // ②

    init { log("init A") }                    // ③
    init { log("init B") }                    // ③

    constructor() : this(0) {                 // ④
        log("secondary body")
    }
}

fun log(tag: String): Int {
    println(tag)
    return 0
}

fun main() { Demo() }

输出结果

param
prop a
prop b
init A
init B
secondary body

可以看到:

  1. paramprop aprop binit Ainit B——全部在进入次级构造体之前完成
  2. secondary body(次构造函数体)最后才执行,一次且仅一次。
  1. 主构造函数与次级构造函数

    • 执行顺序
      Kotlin 规定必须先执行主构造函数,再执行次级构造函数中的代码。因此,次级构造函数必须先委托调用主构造函数。

    • 成员变量和 init 代码块
      成员变量的初始化和 init 代码块的执行顺序是按照它们在类中声明的顺序进行的。

    • 后续处理
      紧接着就是属性的初始化。如果是 data class,还会自动生成并初始化一些方法,例如:

      • equals()
      • hashCode()
      • toString()
      • copy()
      • componentsN() 系列函数

      备注:在 data class 中,== 表示调用 equals() 方法,而 === 则用于比较引用地址。如果重写了 equals() 方法,== 会比较对象的内容(按你定义的逻辑)。如果没有重写 equals() 方法,== 实际上会比较对象的引用,类似于 === 运算符。


二、中缀函数(infix)

Kotlin 支持中缀函数,使得调用方式更加自然。可以使用空格直接调用函数了,而不是用 "."。以下以示例说明如何将普通函数改写为中缀函数:

普通函数:

fun <T> T?.showInfo(default: String = "对象为空"): T? {
    println(this ?: default)
    return this
}
fun main() {
    val a: String? = null
    a.showInfo()
}

改为中缀函数:

infix fun <T> T?.showInfo(default: String): T? {
    println(this ?: default)
    return this
}
fun main() {
    val a: String? = null
    a showInfo ("对象为空!")
}

说明:中缀函数要求函数名位于变量两侧,使得代码看起来更像自然语言表达,调用时函数关键字处于中间。Kotlin 的 infix 函数不支持默认值参数。如果你需要默认值,请使用普通函数或提供函数的重载。


三、内联函数(inline)及相关关键字

由于 Java 对函数类型的原生支持有限,Kotlin 提供了内联函数(inline)以优化 lambda 表达式的性能,避免每次调用时创建新的函数对象,并降低调用堆栈深度。具体说明如下:

1. inline 的作用

  • 内联优化
    内联函数在编译时会将函数体直接插入到调用处,避免函数调用开销。例如:

    fun hello(postAction: () -> Unit) {
        println("Hello!")
        postAction()
    }
    
    // 调用处
    fun main() {
        hello {
            println("Bye!")
        }
    }
    

    不使用内联的时候,编译出来的结果是这样的:

    fun main() {
        val post = object : Function0<Unit> {
            override fun invoke() {
                return println("Bye!")
            }
        }
    }
    
    

使用内联的结果相当于:

fun main() {
    println("Hello!")
    println("Bye!")
}

这样就不再创建 lambda 对象,性能更高,尤其在循环和高频调用场景中效果显著。最好是在频繁使用的高阶函数处使用inline,否则可能造成包体积的负优化等。

2. noinline

用在参数上的关键字,局部关闭内联优化。

  • 作用
    对于内联函数的参数,默认情况下 lambda 表达式会被内联。如果不希望某个 函数类型的 参数被内联,则可以使用 noinline 修饰。函数类型的参数本质上是一个对象,可以把对象当做函数来调用,这也是最常见的做法,但是同时也可以当做对象来用,比如当做返回值。但是当我们把这些函数都做成内联的时候,他们就不再是对象了:
inline fun hello(preAction: () -> Unit, postAction: () -> Unit): ()->Unit {
    preAction()  // 做前置工作
    println("Hello!")
    postAction()  // 做点后续工作
    return postAction // 需要返回值
}

fun main() {
    hello({
        println("Emm...")
    }, {
        println("Bye!")
    })
}

// 因为hello有inline关键字,那么实际的代码就变成了这样:
fun main() {
    println("Emm...")
    println("Hello!")
    println("Bye!")
    // postAction 这一行就会报错。
}

如果还想要返回其中的函数型类型的参数的话,比如preAction或者是postAction,那么就需要在他们前边加上noinline 关键字。

inline fun hello(preAction: () -> Unit, noinline postAction: () -> Unit): ()->Unit {
    preAction()  // 做前置工作
    println("Hello!")
    postAction()  // 做点后续工作
    return postAction // 需要返回值
}

// 上边的代码编译之后的效果
fun main() {
    println("Emm...")
    println("Hello!")
    val postAction = ({
        println("Bye!")
    }).invoke()
    postAction
}

注意:被 noinline 修饰的 函数类型的参数 将不会参与内联优化,因而需要实际创建函数对象。

3. crossinline

用在参数上的关键字。

✦ 一句话结论

crossinline 禁止 Lambda 里的“非局部 return” ,既能保持参数被内联(inline 带来的零开销),又能安全地把该 Lambda 放到其他线程、回调或高阶函数中执行。


1 为什么需要 crossinline

场景问题crossinline 解决
内联函数把 Lambda 直接展开到调用处Lambda 里写 return 会尝试直接返回到外层调用者如果 Lambda 被异步调用,这个 “跳出去” 行为会让编译器报错
异步执行转手传递 Lambda但仍想保留 inline 带来的性能crossinline,禁止 Lambda 用 return 跳出,让它安全“逃逸”

2 代码示例

image.png 在内联函数中,删除crossinline之后,无法在一个异步线程中执行函数参数了。这是因为kotlin是为了避免这样一种情况,所才从语法层面完全禁止这种调用:doSomething 被标记为 inline,这意味着它的参数 action 理论上可以在 Lambda 里写裸 return 来“跳出” doSomething

doSomething {
    println("before")
    return          // 按规则:应直接结束 doSomething 的调用
}

问题出在执行位置

Thread {
    action()        // ← 这里并不是在 doSomething 的 *同一栈帧* 中执行
    return@Thread
}.start()
  1. action() 被包进 Runnable,真正执行要等新线程启动。
  2. 如果 action 里写了裸 return,它会试图回到已经不存在的 doSomething 栈帧 → 非法跳转
  3. 编译器因此禁止这种调用,并给出错误提示。 crossinline 禁止action 内部出现裸 return,从源头杜绝“跨线程跳栈”。这样编译器就放心让我们把 action 延迟 到别的线程/回调里执行。
inline fun doSomething(crossinline action: () -> Unit) {
    // Lambda 被封装进 Runnable,稍后在新线程执行
    Thread {
        action()          // ✅ 可以调用
        // action() 内部不能写 return;只能用 return@action 或省略,只结束 Runnable.run(),不会企图跳到 doSomething
    }.start()
}

fun main() {
    doSomething {
        println("Running in another thread")
        // return      // ❌ 编译错误:crossinline 禁止非局部 return
        return@doSomething // ✅ 可以,局部返回
    }
}

要点

  • 如果去掉 crossinline,编译器会因“Lambda 可能在函数返回后执行”而拒绝编译。
  • crossinline 让 Lambda 依旧 inline,但把 return 改限制为只能局部返回(用 return@label)。

编译器这么限制是为了 类型 & 控制流安全
如果允许裸 return,Lambda 可能在未来某个时刻(甚至另一个线程)才执行,
这时外层函数早就返回,栈帧已不存在,直接跳回去会破坏 JVM 的正常执行。


3 与其他关键字对比

关键字是否内联是否允许非局部 return典型用途
inline(默认)✔︎✔︎普通高阶函数,Lambda 当场执行
noinline✖︎✖︎需要把 Lambda 当对象存储或返回
crossinline✔︎✖︎Lambda 异步/延迟调用,仍想省掉对象开销

4. reified

  • 作用
    Kotlin 中泛型在运行时会被擦除,无法直接获取具体类型。通过内联函数结合 reified 关键字,可以在函数内部获取泛型的实际类型。

示例1:

inline fun <reified T> test(t: T) {
    val c = T::class.java  // 编译成功,能获取到 T 对应的 Class 对象
}

注意:只有在内联函数中,使用 reified 才有意义,因为编译器需要在调用处将泛型信息写入代码中。reified 关键字通常与 inline 函数一起使用,它可以让泛型类型参数在运行时保留具体的类型信息。通常由于类型擦除(type erasure),泛型在运行时是不可见的,但是使用 reified 后,你可以在函数体内直接使用类型参数,例如做类型检查或者获取 KClass 对象。

示例2

inline fun <reified T> isInstance(value: Any): Boolean {
    return value is T
}

fun main() {
    println(isInstance<String>("Hello")) // 输出 true
    println(isInstance<Int>("Hello"))    // 输出 false
}

在上面的代码中,由于 T 被标记为 reified,所以在函数体内可以直接使用 is T 来判断传入的值是否为该类型。如果没有 reified 标记,就无法在运行时获取 T 的类型信息,从而无法进行这样的操作。

注意事项

  • 仅适用于 inline 函数: reified 类型参数只能用于 inline 函数,因为只有在内联的场景下,编译器才能将泛型的具体类型信息嵌入到调用处的代码中。
  • 类型安全: 使用 reified 可以帮助你写出更加类型安全的代码,因为你可以在运行时进行类型判断,而无需显式传递 Class 或者 KClass 对象。

四、枚举(Enum)

  • 常用静态方法

    • values():返回枚举中所有值的数组。
    • valueOf(String):根据名称返回对应的枚举常量。
  • 成员属性

    • name:枚举常量的名称。
    • ordinal:枚举常量的序号。
  • 其它

    • 枚举支持匿名内部类,可以为枚举常量重写方法。
    • 枚举可以实现接口,但不能继承其他类。

下面是一个综合示例,展示了枚举的常用静态方法、成员属性、匿名内部类以及实现接口的用法:

// 定义一个接口,枚举将实现这个接口
interface Operation {
    fun apply(a: Int, b: Int): Int
}

// 定义一个枚举类实现 Operation 接口
enum class MathOperation : Operation {
    // 每个枚举常量通过匿名内部类重写 apply 方法,实现各自的运算逻辑
    ADD {
        override fun apply(a: Int, b: Int) = a + b
    },
    SUBTRACT {
        override fun apply(a: Int, b: Int) = a - b
    },
    MULTIPLY {
        override fun apply(a: Int, b: Int) = a * b
    },
    DIVIDE {
        override fun apply(a: Int, b: Int) = if (b != 0) a / b else throw IllegalArgumentException("除数不能为0")
    }
}

fun main() {
    // 使用 values() 方法,获取枚举中所有的常量
    val operations = MathOperation.values()
    println("枚举中的所有运算:")
    for (op in operations) {
        // 使用 name 和 ordinal 属性
        println("名称:${op.name},序号:${op.ordinal}")
    }
    
    // 使用 valueOf() 方法,根据名称获取枚举常量
    val opName = "MULTIPLY"
    val op = MathOperation.valueOf(opName)
    val result = op.apply(6, 7)
    println("\n使用 valueOf() 获取 '$opName' 后,6 和 7 的运算结果为:$result")
    
    // 演示枚举实现接口的调用
    println("\n通过接口调用运算方法:")
    for (operation in MathOperation.values()) {
        println("${operation.name} 运算:6 和 7 的结果是 ${operation.apply(6, 7)}")
    }
}

说明

  1. 常用静态方法:

    • MathOperation.values() 返回枚举中所有常量的数组。
    • MathOperation.valueOf(opName) 根据给定名称返回对应的枚举常量。
  2. 成员属性:

    • 每个枚举常量都有 name 属性,表示常量的名称。
    • ordinal 属性表示枚举常量在枚举中的序号(从 0 开始)。
  3. 匿名内部类:

    • 每个枚举常量通过大括号内的代码块,重写了 apply 方法,实现了各自不同的运算逻辑。
  4. 实现接口:

    • 枚举类 MathOperation 实现了接口 Operation,所以所有枚举常量都必须提供 apply 方法的实现。
    • 注意,枚举不能继承其他类,因为它们默认继承自 Enum 类,但可以实现一个或多个接口。

五、可空类型与基本类型

  • Kotlin 中不存在原始基本类型,所有数据类型都是对象。
  • 对于可空类型,编译后会转换为相应的包装类型;而非可空类型则尽可能使用 JVM 原生的基本类型,这样可以获得更好的性能。
  • 这种设计使得类型转换操作更加透明,必须由开发者主动处理类型转换问题。

六、委托(Delegation)

Kotlin 的委托机制分为类委托和属性委托,分别对应 Java 中的代理模式和属性访问代理。

1. 类委托

示例 1:基础接口委托

通过实现接口,并在类内部持有一个同类型成员,可以将大部分接口实现委托出去。

// 定义一个接口
interface Printer {
    fun print(message: String)
}

// 实现接口的一个具体类
class SimplePrinter : Printer {
    override fun print(message: String) {
        println("Printing: $message")
    }
}

// 通过接口委托,把 Printer 的实现委托给传入的 delegate 对象
class DelegatingPrinter(private val delegate: Printer) : Printer by delegate

fun main() {
    val printer = SimplePrinter()
    val delegatingPrinter = DelegatingPrinter(printer)
    delegatingPrinter.print("Hello, Kotlin!")
}

说明:
DelegatingPrinter 使用 : Printer by delegatePrinter 接口的大部分实现委托给了 delegate 对象(这里是 SimplePrinter 的实例),从而无需重复实现所有方法。


示例 2:添加扩展功能

如果需要在委托的基础上添加额外的功能,可以在委托的同时重写部分方法。

// 接口同上
interface Printer {
    fun print(message: String)
}

// 基础实现类
class SimplePrinter : Printer {
    override fun print(message: String) {
        println("Printing: $message")
    }
}

// 扩展类,通过委托实现接口,并在部分方法中添加额外功能
class LoggingPrinter(private val delegate: Printer) : Printer by delegate {
    // 重写 print 方法,在调用委托对象之前和之后做一些日志输出
    override fun print(message: String) {
        println("Log: 开始打印")
        delegate.print(message)
        println("Log: 打印结束")
    }
}

fun main() {
    val printer = SimplePrinter()
    val loggingPrinter = LoggingPrinter(printer)
    loggingPrinter.print("Hello, Kotlin with logging!")
}

说明:
LoggingPrinter 中,使用 : Printer by delegate 委托了接口实现,但重写了 print 方法,从而在调用实际打印前后添加了日志功能。这展示了如何在委托的基础上进行局部定制。


示例 3:局部定制与功能扩展

通过委托机制,可以只定制接口中的部分方法,而其他方法则保持原有实现不变。下面以数据存储为例,展示如何在加载数据时添加缓存功能。

// 定义数据存储接口
interface DataStore {
    fun save(data: String)
    fun load(): String
}

// 文件存储的具体实现
class FileDataStore : DataStore {
    override fun save(data: String) {
        println("将数据保存到文件:$data")
    }
    
    override fun load(): String {
        println("从文件中加载数据")
        return "file data"
    }
}

// 缓存存储类:委托了 DataStore 接口,但重写了 load 方法,实现缓存功能
class CachingDataStore(private val delegate: DataStore) : DataStore by delegate {
    private var cache: String? = null
    
    override fun load(): String {
        if (cache == null) {
            cache = delegate.load()
        }
        println("返回缓存数据:$cache")
        return cache!!
    }
}

fun main() {
    val fileStore = FileDataStore()
    val cachingStore = CachingDataStore(fileStore)
    
    // 第一次调用 load,会从文件中加载
    println(cachingStore.load())
    // 后续调用 load,则直接返回缓存的数据
    println(cachingStore.load())
    
    // save 方法没有重写,依然使用委托对象的实现
    cachingStore.save("New data")
}

说明:
CachingDataStore 中,我们对 load 方法进行了局部定制,添加了缓存逻辑,而 save 方法则完全委托给 FileDataStore。这样既复用了原有功能,又能根据需要增加新的特性。

2. 属性委托

  • 说明
    Kotlin 还支持将属性的 getter 和 setter 委托给另一个对象。这种机制称为属性委托。常见的委托有 lazyobservablevetoable 等。如果自定义的话,委托类需要使用 operator 修饰符来实现 getValue()setValue() 方法,其参数必须包含委托属性所属的类(Owner)和属性信息(KProperty)。

2.1 延迟属性 (Lazy)

延迟属性在首次访问时进行初始化,并将结果缓存起来。

示例

val lazyValue: String by lazy {
    println("Computing the value...")
    "Hello"
}

fun main() {
    println(lazyValue)  // 第一次访问时会计算并打印 "Computing the value..." 和 "Hello"
    println(lazyValue)  // 后续访问直接返回缓存值,不再计算
}

2.2 可观察属性 (Observable)

通过 Delegates.observable 可以在属性变化时自动调用回调函数。

示例

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") { prop, old, new ->
        println("属性 ${prop.name}$old 变为 $new")
    }
}

fun main() {
    val user = User()
    user.name = "Alice"  // 触发回调,打印:属性 name 由 <no name> 变为 Alice
    user.name = "Bob"    // 触发回调,打印:属性 name 由 Alice 变为 Bob
}

2.3 可限制修改属性 (Vetoable)

Delegates.vetoable 允许在属性值改变前进行验证,决定是否接受这个新值。

示例

import kotlin.properties.Delegates

class User {
    var age: Int by Delegates.vetoable(0) { _, old, new ->
        new >= old  // 只有新值大于或等于旧值时才允许修改
    }
}

fun main() {
    val user = User()
    user.age = 18
    println(user.age)  // 输出 18
    user.age = 16      // 由于新值小于旧值,修改被拒绝
    println(user.age)  // 依然输出 18
}

2.4 自定义属性委托

这种委托只能用于顶层属性、类成员属性或扩展属性,不能用于方法内部的局部变量,因为局部变量没有生成标准的 get/set 方法。

属性委托 (delegated property) 的本质:
把读取 (get) 与写入 (set) 的实现交给“委托对象”——一个实现了 getValue/setValue 运算符函数的对象
这个“对象”通常是你 new 出来的一个 小类实例,而不是某个已存在的属性。

  • 示例1:简单自定义属性委托

1. 委托实现

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/** 确保属性始终 ≥ 0,违法时抛 IllegalArgumentException */
class NonNegativeInt(init: Int = 0) : ReadWriteProperty<Any?, Int> {

    private var field = init

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int = field

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        require(value >= 0) {
            "${property.name} must be non-negative (got $value)"
        }
        field = value
    }
}

使用方式

class Player {
    var score by NonNegativeInt()   // 默认为 0
}

fun main() {
    val p = Player()
    p.score = 10          // ✅ OK
    p.score = -3          // ❌ IllegalArgumentException
}

要点

  • 业务类只写一行 var score by NonNegativeInt()
    校验逻辑完全藏在委托里。
  • 若后期要改成「最大值 9999」或加埋点,只需改委托,不动业务代码。
  • 示例2:自定义委托实现

    
    // **写法差异** 只体现在 *代码层面的可读性、复用性*:
    // -   `Simple07` **直接声明运算符函数**,轻量、语法最短;
    // -   `StringDelegate` **实现标准接口 `ReadWriteProperty`**,范型可配、IDE 能补全、更通用。
    
    class Owner {
        var text: String by Simple07()
        var text2: Int by StringDelegate()
    }
    
    class Simple07 {
        private var str: String = "Default"
        operator fun getValue(owner: Owner, property: KProperty<*>): String {
            println("Simple07 getValue 执行啦")
            return str
        }
        operator fun setValue(owner: Owner, property: KProperty<*>, value: String) {
            println("Simple07 setValue 执行啦")
            str = value
        }
    }
    
    class StringDelegate : ReadWriteProperty<Owner, Int> {
        private var number: Int = 10000
        override fun getValue(thisRef: Owner, property: KProperty<*>): Int {
            println("ReadWriteProperty getValue 执行啦")
            return number
        }
        override fun setValue(thisRef: Owner, property: KProperty<*>, value: Int) {
            println("ReadWriteProperty setValue 执行啦")
            number = value
        }
    }
    

如何使用:

fun main() {
    val o = Owner()

    println("初始 text  = ${o.text}")   // 触发 Simple07.getValue
    o.text = "Hello"
    println("修改后 text = ${o.text}")  // 再次触发 getValue

    println("初始 text2 = ${o.text2}")  // 触发 StringDelegate.getValue
    o.text2 = 42
    println("修改后 text2 = ${o.text2}")// 触发 setValue + getValue
}
  • 示例3:对外只读、对内可修改的集合委托
    如果需要实现一个集合,对外暴露只读接口,但内部可修改,可以使用属性委托:

    class Model2 {
        // 对外暴露不可修改的 List,通过委托到内部的 MutableList
        val data: List<String> by ::_data
        // 内部使用 MutableList,可以修改数据
        private val _data: MutableList<String> = mutableListOf()
        
        fun load() {
            _data.add("Hello") // 内部可以修改
        }
    }
    
    fun main() {
        val model = Model2()
        // model.data.add("Hello") // 编译错误,因为 data 是只读 List
        // val localValue: String by lazy { "Hello, World!" } // 从 Kotlin 1.4 开始,局部委托属性是允许的。
        println(model.data)
    }
    

注意:委托只能用于属性委托或类委托,从 Kotlin 1.4 开始,局部委托属性是允许的。之前的版本会报错。

七、Nothing

Nothing 是所有类型的子类型。它有3个作用。

  • 第一个作用,作为函数的返回值,表达这个函数永不返回,抛异常或者无限循环的场景
fun foreverRepeat(): Nothing {
    while (true) {
        // 无限循环的代码
    }
}

  • 第二个主要用途,用来作为泛型变量的一个通用的空白占位置:
val emptyList: List<Nothing> = listOf()

var apples: List<Apple> = emptyList
var users: List<User> = emptyList
var phones: List<Phone> = emptyList
var images: List<Image> = emptyList

val emptyProducer: Producer<Nothing> = Producer()

var appleProducer: Producer<Apple> = emptyProducer
var userProducer: Producer<User> = emptyProducer
var phoneProducer: Producer<Phone> = emptyProducer
var imageProducer: Producer<Image> = emptyProducer
  • 第三个用途,让代码从语法层面得到解释,完善语法,比如return 返回的就是Nothing.

一些问题

1、不同高阶函数的一些区别

fun runAsUser(block: (Int.() -> Unit)) {
    1.block() // 或者写成 value.block()
}

fun runAsUser(block : (Double)->Unit) {
    block()
}

上边的两种写法编译之后是这样的:

public final void runAsUser(@NotNull Function1 block) { 
    Intrinsics.checkNotNullParameter(block, "block");
    block.invoke(1); 
}

public final void runAsUser(@NotNull Function1 block) { 
    Intrinsics.checkNotNullParameter(block, "block"); 
    block.invoke(1.0); 
}

可见block: (Int.() -> Unit,block : (Double)->Unit 编译之后并没有区别,只是一个有隐式的receiver,而另一个没有罢了。

学习速查

问题答案
1Kotlin 中对象的完整初始化顺序?① 主构造函数参数默认值 → ② 属性声明顺序 → ③ init 块顺序 → ④ 次级构造函数体(次级构造函数必须先委托主构造函数)。 栈示例:<主构造> → prop1 → prop2 → initA → initB → <次级构造体>
2infix 函数的两条语法限制?① 必须是成员函数或扩展函数;② 只能有一个参数且不能带默认值
3inline 带来哪两层性能收益?① 消除函数调用栈;② 避免为每次 Lambda 调用生成 Function 对象(减少堆分配 + GC)。
4什么时候给 inline 参数加 noinline当该 Lambda 需要作为对象存储、返回或传递时,否则被内联后就不再是对象。
5crossinline 解决了什么问题?保留内联零开销的同时,禁止 Lambda 的非局部 return,从而可安全地在异步线程/回调里执行该 Lambda。
6如何同时用 noinline+crossinline把一个 Lambda 返回(noinline)给外部缓存,另一个 Lambda 异步执行(crossinline);两者共存同一个 inline 函数。
7为什么 reified 只能出现在 inline 函数中?编译器需在调用点注入字节码以保留实际泛型类型;非内联函数没有此展开机会。
8Kotlin 枚举常量如何重写接口方法?编译器把每个常量生成成 匿名内部类,这些类继承同一个 Enum 子类并各自覆盖方法。
9Int? 何时会装箱?只要出现可空类型或泛型擦除场景就会 boxed;纯 Int 非可空且非泛型时编译为 int。边界例:listOf<Int?>() 里的元素会装箱。
10类委托 vs. Decorator 最大区别?类委托由 语言级语法自动转发全部接口方法,可选局部重写;Decorator 需手工转发,样板多。
11写一个磁盘缓存属性委托的核心思路?getValue:内存缓存为空→读文件→缓存;setValue:写文件→刷新缓存→发送事件。
12为什么 List<Nothing> 能赋值给 List<Apple>Nothing 是一切类型的协变子类型,充当“永不产生元素”的生产者占位符,类型安全。
13(Int.() -> Unit)(Int) -> Unit 编译后差异?前者多了隐藏接收者参数,字节码签名仍是 Function1<Integer, Unit>,但可重载因 Kotlin 语义不同。
14Delegates.observableStateFlow 区别?前者同步回调、无背压、不具备冷流语义;后者是热流、支持多订阅、具备背压与协程取消。
15局部 by lazy 线程安全等级?使用 LazyThreadSafetyMode.SYNCHRONIZED — 即与全局 lazy 相同:首次访问加锁,保证多线程安全,局部变量作用域结束即被 GC。