Android随笔-kotlin 高阶函数

Kotlin 的高阶函数(Higher-Order Function)是指以函数作为参数,或者返回值为函数的函数。

一、两种基本形式

1. 函数作为参数

kotlin 复制代码
// 定义:接收一个函数参数
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

// 使用:传入 Lambda
val sum = calculate(3, 4) { x, y -> x + y }      // 7
val product = calculate(3, 4) { x, y -> x * y }  // 12

2. 函数作为返回值

kotlin 复制代码
// 定义:返回一个函数
fun multiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }
}

// 使用
val triple = multiplier(3)
println(triple(5))  // 15

二、为什么需要高阶函数?

代码复用与抽象:把变化的行为抽出来,骨架保持不变。

kotlin 复制代码
// 通用模板:只关心"执行前后"的逻辑
fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("耗时:${end - start} ms")
}

// 使用:传入任意逻辑
measureTime {
    // 这里放你想测量的代码
    Thread.sleep(1000)
}

Kotlin 标准库中的经典高阶函数

函数 作用 示例
let 非空执行 + 转换 str?.let { it.length }
run 执行代码块并返回结果 obj.run { name.length }
apply 配置对象并返回自身 TextView().apply { text = "Hi" }
also 副作用操作 list.also { println(it) }
with 对对象批量操作 with(buffer) { append("a"); append("b") }
filter / map 集合操作 list.filter { it > 0 }

这些本质上都是内联高阶函数(inline),编译时会展开,没有额外函数调用开销。

三、内联优化:inline 关键字

inline 的本质是编译期代码替换(Code Inlining )。它不是运行时的优化,而是编译器直接把函数体和 Lambda 代码"复制粘贴"到调用处。

当高阶函数配合 Lambda 使用时,每次调用都会产生一个匿名类的对象(带来性能开销)。用 inline 可以消除这个开销:

kotlin 复制代码
inline fun repeatAction(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) {
        action(i)
    }
}

// 编译后,Lambda 代码直接嵌入调用处,不生成额外对象
repeatAction(3) { println("第 $it 次") }

注意:inline 适合短小、频繁调用的高阶函数;如果 Lambda 参数被存储(而非立即调用),则不能用 inline。

1. 非 inline 高阶函数的编译结果

kotlin 复制代码
fun measure(block: () -> Unit) {
    println("start")
    block()
    println("end")
}

fun main() {
    measure { println("hello") }
}

编译后(近似 Java 字节码):

java 复制代码
// 1. 编译器为 Lambda 生成匿名类
class Main$1 implements Function0<Unit> {
    public Unit invoke() {
        println("hello");
        return Unit.INSTANCE;
    }
}

// 2. 调用处:创建对象 + 方法调用
void main() {
    measure(new Main$1());  // ① 创建函数对象
}

// 3. 高阶函数本身:虚方法调用
void measure(Function0<Unit> block) {
    println("start");
    block.invoke();         // ② 虚方法调用(invoke)
    println("end");
}

开销来源

  1. 对象分配:new Main$1() --- Lambda 被编译成匿名类实例
  2. 方法调用:measure() 本身是一次函数调用
  3. 虚方法分派:block.invoke() 是接口虚调用,JVM 无法内联优化
  4. 装箱/捕获开销:如果 Lambda 引用了外部变量,这些变量要封装进匿名类的字段

2. 加了 inline 后的编译结果

kotlin 复制代码
inline fun measure(block: () -> Unit) {
    println("start")
    block()
    println("end")
}

fun main() {
    measure { println("hello") }
}

编译后,main 函数直接变成:

java 复制代码
void main() {
    println("start");
    println("hello");   // Lambda 代码直接嵌入,没有 invoke 调用
    println("end");
}

3. 消除了哪些具体开销?

开销类型 非 inline inline 说明
函数对象创建 new Function0() ❌ 无 不需要生成匿名类,不分配堆内存
方法调用 调用 measure() ❌ 无 函数体直接展开在调用处
Lambda 调用 block.invoke() ❌ 无 Lambda 代码直接嵌入,无虚方法分派
变量捕获装箱 封装到匿名类字段 ❌ 无 外部变量直接以内联方式使用
栈帧创建 ❌ 无 不进入新的函数栈帧

4. 直观对比

复制代码
非 inline 调用链:
main() → measure() → block.invoke() → println()
   ↑         ↑            ↑
  栈帧      栈帧         虚方法分派

inline 调用链:
main() → println("start") → println("hello") → println("end")
   ↑
  只有一个栈帧,全部顺序执行

5. 代价(Trade-off)

inline 不是免费的,它用代码体积换执行效率

  • 每处调用都会复制一份函数体,导致生成的字节码变大(Code Bloat)
  • 如果函数体很大或被大量调用,会显著增加包体积
  • 因此 Kotlin 官方建议:只给短小、高频的高阶函数加 inline

四、优点

优点 说明
代码复用 把变化的行为抽成参数,骨架代码复用(如 measureTimerepeatAction
声明式编程 描述"做什么"而非"怎么做",代码更简洁(如 list.filter { it.age > 18 }
语法简洁 配合 Lambda,避免大量接口+实现类的样板代码
标准库丰富 let/run/apply/also/withfilter/map/reduce 等全是高阶函数
内联优化 inline 关键字可消除 Lambda 对象创建的开销,兼顾简洁与性能

五、缺点 & 坑点

缺点 说明
性能开销 inline 的高阶函数,每次调用会创建匿名类对象(Kotlin 1.5+ 对非捕获 Lambda 有优化,但捕获变量时仍有开销)
可读性下降 Lambda 嵌套过深(如回调地狱)会让代码难读
调试困难 堆栈信息里全是 Lambda$1$inline$ 之类的符号,断点定位麻烦
非局部返回陷阱 Lambda 里直接写 return 会返回到外层函数,容易误用
类型推断局限 复杂泛型 + 高阶函数组合时,编译器推断失败,需要显式声明类型
与 Java 互操作 Java 调用 Kotlin 高阶函数时语法不友好(需传入 FunctionN 实例)

六、常见问题

Q1:什么是高阶函数?和普通函数的区别?

接收函数作为参数,或返回值为函数的函数。普通函数操作的是数据,高阶函数操作的是行为。

Q2:inline 的作用是什么?为什么要内联?

inline 在编译期将函数体和 Lambda 代码直接插入调用处,消除函数调用和匿名对象创建的开销。适合短小、频繁调用的高阶函数。

Q3:inline、noinline、crossinline 的区别?

关键字 作用
inline 函数内联展开,Lambda 参数默认也内联
noinline 标记某个 Lambda 参数不内联(需要作为对象存储或传递时使用)
crossinline Lambda 禁止非局部返回(用于内联函数中,Lambda 在另一个上下文执行的情况,如线程切换)

Q4:什么是非局部返回(Non-local return)?

内联函数 的 Lambda 中直接使用 return,会返回到调用该内联函数的外层函数,而非仅退出 Lambda。这是内联特性带来的特殊行为。

kotlin 复制代码
fun foo() {
    run {  // inline 函数
        return  // 直接返回到 foo(),不是只退出 run!
    }
    println("不会执行")
}

Q5:let、run、apply、also、with 的区别?

函数 接收者 参数 返回值 典型用途
let it T.() -> R Lambda 结果 非空判断 + 转换
run this T.() -> R Lambda 结果 对象计算并返回结果
apply this T.() -> Unit 对象自身 对象配置(Builder 模式)
also it T.() -> Unit 对象自身 副作用(打印日志)
with this T.() -> R Lambda 结果 对同一对象批量操作

带 a 的(apply、also)返回自身;带 t 的(let)用 it;带 this 的是扩展接收者。

Q6:高阶函数在编译后是什么样子的?

非内联的高阶函数编译后,Lambda 会被编译成匿名类(实现 FunctionN 接口),捕获的变量作为其成员字段。inline 函数则直接展开代码,不生成额外类。

Q7:Kotlin 的函数类型和 Java 的 SAM 接口有什么区别?

  • Kotlin 的函数类型(如 (Int) -> Int)是编译器层面的语法糖,底层是 Function0、Function1 等接口。
  • Java 的 SAM(Single Abstract Method)接口需要显式定义接口类型。
  • Kotlin 支持自动 SAM 转换(Java 接口只有一个抽象方法时,可用 Lambda 替代),但 Kotlin 接口不支持 SAM 转换(需用函数类型或显式实现)。

Q8: 实现一个类似 measureTime 的高阶函数,要求内联,并支持返回耗时和结果。

kotlin 复制代码
inline fun <T> measureTimeMillis(block: () -> T): Pair<T, Long> {
    val start = System.currentTimeMillis()
    val result = block()
    val cost = System.currentTimeMillis() - start
    return result to cost
}

// 使用
val (data, time) = measureTimeMillis {
    fetchDataFromNetwork()
}

七、总结

Kotlin 高阶函数是函数式编程的基石,通过将行为参数化 实现代码复用;配合 inline 消除性能开销,但需注意非局部返回和可读性问题。标准库中的 let/run/apply 等本质上都是内联高阶函数

相关推荐
小孔龙2 小时前
GraphicBuffer 跨进程共享
android
行业研究员2 小时前
Android内置GPS与腾讯地图定位技术对比分析
android·android定位·腾讯地图定位
Android-Flutter3 小时前
android WMS 详解(二)
android·kotlin
游戏开发爱好者84 小时前
App Store 上传 IPA 自动化,.p8 密钥认证与 CI/CD 接入实战
android·运维·ci/cd·小程序·uni-app·自动化·iphone
游戏开发爱好者85 小时前
iOS 推送怎么配置,APNs 推送证书、设备库与群发
android·ios·小程序·https·uni-app·iphone·webview
wxson728220 小时前
【Android视频监控系统技术总结】
android·音视频
hunterandroid21 小时前
[Android 从零到一] LiveData 粘性事件与单次消费:从重复触发到可靠事件总线
android
hunterandroid1 天前
[Android 从零到一] Android 事件分发机制:从 ACTION_DOWN 到 View 事件消费链路
android