深入理解!Kotlin 高阶函数与内联函数:noinline、crossinline 那些坑都替你踩过了!
深入理解!Kotlin 高阶函数与内联函数:noinline、crossinline 那些坑都替你踩过了!
目录
- 高阶函数基础
- Lambda 表达式详解
- 常用高阶函数
- 内联函数原理
- noinline 详解
- crossinline 详解
- 内联函数的限制
- 实战应用
- 常见错误与解决方案
1. 高阶函数基础
1.1 什么是高阶函数?
高阶函数是接收函数作为参数或返回函数的函数。
普通函数:参数是值,返回值是值 高阶函数:参数可以是函数,返回值也可以是函数1.2 基本语法
// 定义一个高阶函数:接收一个函数作为参数funcalculate(a:Int,b:Int,operation:(Int,Int)->Int):Int{returnoperation(a,b)}// 使用valresult=calculate(10,5){x,y->x+y}// 15valresult2=calculate(10,5){x,y->x-y}// 51.3 返回函数的函数
// 返回一个函数funoperation(type:String):(Int,Int)->Int{returnwhen(type){"add"->{a,b->a+b}"sub"->{a,b->a-b}"mul"->{a,b->a*b}else->{a,b->0}}}// 使用valadd=operation("add")println(add(10,5))// 151.4 函数类型
// 无参数,无返回值()->Unit// 有参数,有返回值(Int,String)->Boolean// 可空函数类型((Int)->Int)?// 带 receiver 的函数类型String.()->Boolean// 相当于 (String) -> BooleanInt.(String)->Int// 相当于 (Int, String) -> Int2. Lambda 表达式详解
2.1 Lambda 语法
// 完整语法valsum:(Int,Int)->Int={x:Int,y:Int->x+y}// 类型推导(常用)valsum={x:Int,y:Int->x+y}// 单参数时可用 itlistOf(1,2,3).map{it*2}// it 是单个参数2.2 Lambda 的本质
Lambda 在 Kotlin 编译后会变成一个匿名类(或函数对象):
// Kotlin 代码valsum={x:Int,y:Int->x+y}// 编译后类似变成class<anonymous>:Function2<Int,Int,Int>{overridefuninvoke(x:Int,y:Int):Int=x+y}valsum=<anonymous>()这意味着每次创建 Lambda 都会产生对象,有性能开销。
2.3 Lambda 的调用
valsum:(Int,Int)->Int={x,y->x+y}// 调用方式1:直接调用sum(1,2)// 调用方式2:invoke 调用sum.invoke(1,2)2.4 Lambda 与成员引用
// Lambdavaldouble={x:Int->x*2}// 成员引用valdouble:(Int)->Int=Int::times3. 常用高阶函数
3.1 let
// let: 在作用域内处理对象,返回最后一行valresult="Hello".let{println("原始值:$it")it.length// 返回这个值}println(result)// 53.2 run
// run: 在对象作用域内执行代码块,返回最后一行valresult="Hello".run{println("原始值:$this")this.length}println(result)// 53.3 with
// with: 调用对象的多个方法时更简洁valresult=with(StringBuilder()){append("Hello")append(" ")append("World")toString()}println(result)// "Hello World"3.4 apply
// apply: 在对象上执行代码块,返回对象本身valperson=Person().apply{name="张三"age=25city="北京"}3.5 also
// also: 类似 apply,但使用 it 引用vallist=mutableListOf<Int>().also{it.add(1)it.add(2)it.add(3)}3.6 takeIf / takeUnless
// takeIf: 条件为 true 返回自身,否则返回 nullvalresult=10.takeIf{it>5}// 10valresult2=10.takeIf{it>20}// null// takeUnless: 条件为 false 返回自身,否则返回 nullvalresult3=10.takeUnless{it>5}// nullvalresult4=10.takeUnless{it>20}// 104. 内联函数原理
4.1 什么是内联?
内联是一种编译器优化技术:将函数调用替换为函数体代码。
// 原始代码inlinefuninlineFunc(x:Int,block:(Int)->Int):Int{returnblock(x)}valresult=inlineFunc(10){it*2}// 编译后实际变成valresult=(10*2)// 函数体直接展开,没有函数调用开销4.2 为什么需要内联?
Lambda 会产生匿名类对象,有性能开销:
普通 Lambda: val result = listOf(1,2,3).map { it * 2 } ↓ 编译后 val result = listOf(1,2,3).map(object : Function1<Int, Int> { override fun invoke(it: Int): Int = it * 2 }) 内联 Lambda: inline fun <T> List<T>.mapInline(transform: (T) -> T): List<T> ↓ 编译后(函数体直接展开) 直接操作,没有额外对象创建4.3 基本用法
// 使用 inline 修饰函数inlinefunmeasureTime(block:()->Unit){valstart=System.currentTimeMillis()block()valend=System.currentTimeMillis()println("耗时:${end-start}ms")}// 使用measureTime{Thread.sleep(100)}4.4 内联函数的工作原理
源代码: inline fun measureTime(block: () -> Unit) { val start = System.currentTimeMillis() block() val end = System.currentTimeMillis() } measureTime { println("Hello") } 编译后(伪代码): val start = System.currentTimeMillis() println("Hello") val end = System.currentTimeMillis() // 函数调用被"展开"成函数体代码4.5 内联的代价
// ⚠️ 内联会使编译后的代码变大(代码膨胀)inlinefunbigFunction(){println("代码段1")println("代码段2")// ... 很大的函数体}// 如果调用 10 次,编译后会有 10 份拷贝5. noinline 详解
5.1 什么是 noinline?
noinline用于告诉编译器:某个函数参数不要内联。
inlinefunfoo(inlined:()->Unit,noinlinenotInlined:()->Unit){inlined()// 会内联notInlined()// 不会内联,保持正常函数调用}5.2 为什么需要 noinline?
内联函数有诸多限制,有时候需要传入非内联的 Lambda:
// ❌ 错误:内联函数的 Lambda 参数不能存储(不能赋值给变量)inlinefunfoo(block:()->Unit){valref=block// 错误!不能存储内联函数的 Lambda}// ✅ 正确:使用 noinlineinlinefunfoo(noinlineblock:()->Unit){valref=block// 可以存储}5.3 noinline 使用场景
// 场景1:需要将 Lambda 传递给非内联函数inlinefunfoo(block:()->Unit){// 传递给另一个需要函数类型参数的高阶函数bar(object:()->Unit{// 需要先转换overridefuninvoke()=block()})}// ✅ 使用 noinline 更简洁inlinefunfoo(noinlineblock:()->Unit){bar(block)// 直接传递}5.4 示例
inlinefuntestInlined(inlinedBlock:()->Unit,noinlinenotInlinedBlock:()->Unit){println("开始")inlinedBlock()// 内联:代码会被展开notInlinedBlock()// 不内联:保持函数调用println("结束")}// 使用testInlined({println("内联块")},// 会被内联{println("非内联块")}// 保持函数调用)6. crossinline 详解
6.1 什么是 crossinline?
crossinline用于修饰不能取消的 Lambda 参数。
6.2 问题:非局部返回
inlinefunfoo(block:()->Unit){block()// 调用 Lambda}// 使用funtest(){foo{println("开始")return// ❌ 问题:这是非局部返回!// 会直接退出 test() 函数,而不是只退出 Lambda}println("这里不会执行")// 永远不会执行}6.3 crossinline 的作用
// crossinline 禁止在 Lambda 中使用 return(非局部返回)inlinefunfoo(crossinlineblock:()->Unit){block()// 仍然可以调用,但 Lambda 内不能使用 return}// 使用funtest(){foo{println("开始")return@foo// ✅ 只能用局部返回(标签返回)// return ❌ 编译错误:不能使用非局部返回}println("这里会执行")// 正常执行}6.4 为什么需要 crossinline?
当内联函数作为参数传递给其他函数时,Lambda 内的 return 不能直接退出外层函数:
// Android 中常见的 Runnable 场景inlinefunpostDelayed(crossinlineblock:()->Unit){handler.post{block()// 这里的 block 会在另一个线程执行}// 如果不用 crossinline,Lambda 内的 return 会导致问题}6.5 noinline vs crossinline
| 修饰符 | 作用 | 使用场景 |
|---|---|---|
inline | 内联函数体到调用处 | 减少函数调用开销 |
noinline | 特定参数不内联 | 需要存储/传递 Lambda 时 |
crossinline | 禁止非局部返回 | Lambda 在其他上下文中执行时 |
6.6 综合示例
// 完整示例inlinefuncomplexFunction(crossinlineonSuccess:()->Unit,// 不能 returnnoinlineonError:()->Unit// 可以存储):()->Unit{// onSuccess 不能 return// onError 可以存储到变量或返回returnonError// 合法:noinline 参数可以作为返回值}// 使用valerrorHandler=complexFunction(crossinline{// return ❌ 编译错误println("成功")},{// return ✅ 可以println("错误")})7. 内联函数的限制
7.1 内联函数不能使用的情况
// ❌ 不能内联 private 成员inlinefunMyClass.privateMethod(){}// 编译错误// ❌ 不能内联带有复杂类型参数的函数inlinefun<T>foo(x:T){}// 可能有问题// ❌ 不能内联 try-catch 块中的函数调用inlinefunfoo(block:()->Unit){try{block()// 某些情况下有限制}catch(e:Exception){}}7.2 具体限制列表
| 限制 | 说明 |
|---|---|
| 不能访问私有成员 | 内联后可能访问不到 |
| 泛型类型擦除 | 内联函数的泛型会被擦除 |
| 不能直接返回函数类型的参数 | 需要用 noinline |
| 非局部返回限制 | 除非用 crossinline |
7.3 递归内联函数
// ❌ 内联函数不能递归调用自身inlinefunrecurse(){recurse()// 编译错误}// ✅ 可以递归,但不能用 inlinefunrecurse(){recurse()}8. 实战应用
8.1 防止重复点击
// 带延迟的防抖函数inlinefunView.setThrottledClick(delayMillis:Long=500L,crossinlineaction:(View)->Unit){varlastClickTime=0LsetOnClickListener{view->valcurrentTime=System.currentTimeMillis()if(currentTime-lastClickTime>delayMillis){lastClickTime=currentTimeaction(view)}}}// 使用button.setThrottledClick{// 处理点击}8.2 测量函数执行时间
inlinefunmeasureTimeMillis(block:()->Unit):Long{valstart=System.currentTimeMillis()block()returnSystem.currentTimeMillis()-start}// 使用valtime=measureTimeMillis{// 执行耗时操作Thread.sleep(100)}println("耗时:${time}ms")8.3 条件执行
inlinefun<T>T.executeIf(condition:Boolean,block:(T)->Unit):T{returnif(condition){block(this)this}else{this}}// 使用"Hello".executeIf(3>1){println(it)// 打印 "Hello"}8.4 资源清理(use)
inlinefun<T:AutoCloseable,R>T.useInline(block:(T)->R):R{returntry{block(this)}finally{close()}}// 使用FileInputStream("file.txt").useInline{stream->// 使用 stream}// 自动关闭8.5 Android 中的应用
// Activity 扩展inlinefunActivity.enableEdgeToEdge(){WindowCompat.setDecorFitsSystemWindows(window,false)}// View 扩展inlinefunView.setVisible(visible:Boolean){visibility=if(visible)View.VISIBLEelseView.GONE}// Context 扩展inlinefunContext.showToast(message:String){Toast.makeText(this,message,Toast.LENGTH_SHORT).show()}9. 常见错误与解决方案
9.1 非局部返回错误
// ❌ 错误inlinefunfoo(block:()->Unit){block()}funtest(){foo{return// 编译错误:不能非局部返回}}// ✅ 解决方案1:使用 crossinlineinlinefunfoo(crossinlineblock:()->Unit){block()}// ✅ 解决方案2:使用标签返回funtest(){foo{return@foo// 局部返回,只退出 Lambda}}9.2 存储 Lambda 错误
// ❌ 错误:不能存储内联函数的 Lambdainlinefunfoo(block:()->Unit){valref=block// 编译错误}// ✅ 解决方案:使用 noinlineinlinefunfoo(noinlineblock:()->Unit){valref=block// 可以存储}9.3 内联函数中的泛型
// ❌ 内联函数的泛型在运行时会被擦除inlinefun<reifiedT>foo(){println(T::class.java)// 需要 reified 才能获取类型}// ✅ 使用 reifiedinlinefun<reifiedT>foo(){println(T::class.java)// 可以获取类型}9.4 选择内联函数的原则
| 情况 | 建议 |
|---|---|
| Lambda 参数调用次数 > 1 | 不内联 |
| Lambda 需要存储/传递 | 用 noinline |
| Lambda 在其他线程执行 | 用 crossinline |
| 函数体很小(几行) | 适合内联 |
| 函数体很大(几十行) | 不适合内联 |
| 递归函数 | 不能内联 |
总结
- 高阶函数:接收函数作为参数或返回函数的函数
- Lambda 表达式:
{ 参数 -> 返回值 },本质是匿名类对象 - 内联函数:用
inline修饰,编译时将函数体展开到调用处,减少调用开销 - noinline:指定某个参数不内联,用于需要存储或传递 Lambda 的场景
- crossinline:禁止非局部返回,用于 Lambda 在其他上下文执行的情况
- 使用场景:性能优化、DSL 构建、扩展函数、防止重复点击等
