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 标准库里的 filtermap 等都是内联的。
  • 类型推断问题:有时候 Kotlin 无法推断 Lambda 的参数类型,需要显式声明。比如 { item: Int -> item > 0 },而不是 { it > 0 }

避坑指南:我曾经在写一个递归的高阶函数时,忘记处理终止条件,结果导致栈溢出。高阶函数虽然灵活,但逻辑复杂度并没有消失,只是转移到了 Lambda 里。写的时候一定要想清楚边界条件。

3.7 总结

高阶函数是 Kotlin 函数式编程的基石。它让代码更简洁、更灵活、更可读。说白了,就是把“做什么”和“怎么做”分离开来,让调用者决定具体的行为。

我个人觉得,掌握高阶函数的关键在于理解“函数类型”这个概念。一旦你习惯了把函数当作值来传递,你会发现很多设计模式都可以用几行 Lambda 替代。

下一章我们会聊 Lambda 表达式和匿名函数,到时候你会看到高阶函数和 Lambda 配合起来有多强大。嗯,先消化一下今天的内容,动手写几个例子试试看。