3、高阶函数:让函数成为你的“一等公民”
好,咱们今天聊点硬核的——高阶函数。
说实话,我刚学 Kotlin 那会儿,看到“高阶函数”这四个字,第一反应是:这玩意儿是不是特别难?后来用多了才发现,它其实就是把函数当成普通变量来用。你想想看,既然变量可以传来传去,那函数为什么不行?
在 Kotlin 里,函数是“一等公民”。这意味着你可以把函数赋值给变量,把它塞进参数列表,甚至让它作为返回值跑出来。嗯,这就是高阶函数的核心。
3.1 什么是高阶函数?
定义很简单:如果一个函数接收函数作为参数,或者返回一个函数,那它就是高阶函数。
我在项目中遇到过不少场景,比如网络请求的回调、列表数据的过滤、事件的监听……这些地方用高阶函数,代码会变得特别清爽。说白了,高阶函数就是让你把“行为”当作数据来传递。
核心定义:
- 参数包含函数类型 → 高阶函数
- 返回值是函数类型 → 高阶函数
- 两者都满足 → 也是高阶函数
3.2 函数类型:给函数一个“身份证”
在 Kotlin 里,每个函数都有自己的类型。就像 Int 表示整数,String 表示字符串一样,函数也有自己的类型签名。
函数类型的写法是这样的:
(参数类型列表) -> 返回值类型
举个例子:
// 一个接收 Int 并返回 String 的函数类型
(Int) -> String
// 一个接收两个 Int 并返回 Boolean 的函数类型
(Int, Int) -> Boolean
// 一个不接收参数也不返回值的函数类型
() -> Unit
我个人习惯把函数类型当作接口来理解。你想想看,接口定义了一组方法签名,而函数类型定义了一个函数的签名。只不过函数类型更轻量,不需要单独声明一个接口。
小技巧:在 IDE 里按住 Ctrl 点击函数,可以看到它的类型。我经常用这个方法来检查函数签名是否匹配。
3.3 函数作为参数:把行为“注入”到函数里
这是高阶函数最常用的场景。你写一个函数,它内部需要执行某个操作,但这个操作的具体内容由调用者决定。这时候,把函数作为参数传进去就对了。
来看一个经典例子——自定义的列表过滤:
fun <T> List<T>.myFilter(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (predicate(item)) {
result.add(item)
}
}
return result
}
// 使用
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenNumbers = numbers.myFilter { it % 2 == 0 }
println(evenNumbers) // [2, 4, 6]
你看,myFilter 接收一个 (T) -> Boolean 类型的函数,这个函数决定了哪些元素被保留。调用的时候,我们用 Lambda 表达式 { it % 2 == 0 } 传进去,代码读起来就像在说“给我过滤出偶数”。
我曾经在做一个数据清洗工具时,需要根据不同的规则过滤数据。如果用传统的接口方式,我得定义一堆接口实现类。用高阶函数,直接传 Lambda 就搞定了,代码量减少了一半不止。
注意:如果 Lambda 是函数的最后一个参数,可以把它写在圆括号外面。这是 Kotlin 的约定,能让代码更简洁。但如果你有多个函数参数,只有最后一个能享受这个待遇。
3.4 函数作为返回值:动态生成行为
这个场景稍微进阶一点,但非常强大。函数作为返回值,意味着你可以根据条件动态生成不同的行为。
举个例子,假设你需要一个计算器,根据运算符返回不同的计算逻辑:
fun getCalculator(operator: String): (Int, Int) -> Int {
return when (operator) {
"+" -> { a, b -> a + b }
"-" -> { a, b -> a - b }
"*" -> { a, b -> a * b }
"/" -> { a, b -> a / b }
else -> throw IllegalArgumentException("未知运算符: $operator")
}
}
// 使用
val add = getCalculator("+")
println(add(3, 4)) // 7
val multiply = getCalculator("*")
println(multiply(3, 4)) // 12
你看,getCalculator 返回的是一个 (Int, Int) -> Int 类型的函数。调用者拿到这个函数后,可以像调用普通函数一样使用它。
我记得有一次做策略模式重构,原本有十几个策略类,每个类就一个方法。用高阶函数返回值的方式,我把它们全部压缩成了几个 Lambda 表达式,代码结构一下子清晰了很多。
关键点:函数作为返回值时,要注意闭包问题。如果返回的函数引用了外部变量,这些变量会被“捕获”到函数内部,即使外部作用域已经结束,它们仍然存在。
3.5 实战:用高阶函数重构回调
咱们来看一个实际项目中的例子。假设你有一个网络请求的封装,传统写法可能是这样的:
// 传统接口方式
interface Callback {
fun onSuccess(data: String)
fun onError(error: Throwable)
}
fun fetchData(callback: Callback) {
// 模拟网络请求
thread {
Thread.sleep(1000)
callback.onSuccess("数据加载完成")
}
}
用高阶函数重构后:
fun fetchData(
onSuccess: (String) -> Unit,
onError: (Throwable) -> Unit
) {
thread {
Thread.sleep(1000)
try {
onSuccess("数据加载完成")
} catch (e: Exception) {
onError(e)
}
}
}
// 使用
fetchData(
onSuccess = { data -> println(data) },
onError = { error -> println("出错了: ${error.message}") }
)
你发现没有?代码变得更直观了。调用的时候,你一眼就能看出成功和失败分别做什么。而且不需要再定义接口和实现类,省去了不少样板代码。
我的建议:在 Kotlin 项目里,能用高阶函数的地方尽量别用接口回调。除非你需要多个方法(比如生命周期回调),否则高阶函数是更轻量的选择。
3.6 常见陷阱与避坑指南
用高阶函数多了,总会遇到一些坑。我把自己踩过的几个分享给你:
- 闭包导致的内存泄漏:如果高阶函数内部持有了外部引用,而这个函数被长期持有(比如静态变量),可能会导致内存泄漏。我曾经在 Android 项目里遇到过,一个 Lambda 持有了 Activity 的引用,导致 Activity 无法被回收。解决方案是使用弱引用,或者确保 Lambda 的生命周期可控。
- 内联函数优化:高阶函数会创建匿名内部类,带来一定的性能开销。对于频繁调用的高阶函数,可以用
inline关键字优化。Kotlin 标准库里的filter、map等都是内联的。 - 类型推断问题:有时候 Kotlin 无法推断 Lambda 的参数类型,需要显式声明。比如
{ item: Int -> item > 0 },而不是{ it > 0 }。
避坑指南:我曾经在写一个递归的高阶函数时,忘记处理终止条件,结果导致栈溢出。高阶函数虽然灵活,但逻辑复杂度并没有消失,只是转移到了 Lambda 里。写的时候一定要想清楚边界条件。
3.7 总结
高阶函数是 Kotlin 函数式编程的基石。它让代码更简洁、更灵活、更可读。说白了,就是把“做什么”和“怎么做”分离开来,让调用者决定具体的行为。
我个人觉得,掌握高阶函数的关键在于理解“函数类型”这个概念。一旦你习惯了把函数当作值来传递,你会发现很多设计模式都可以用几行 Lambda 替代。
下一章我们会聊 Lambda 表达式和匿名函数,到时候你会看到高阶函数和 Lambda 配合起来有多强大。嗯,先消化一下今天的内容,动手写几个例子试试看。