第七章:高阶函数——函数也能当参数?没错,Kotlin 就是这么灵活

说实话,我第一次接触高阶函数这个概念时,心里是有点懵的。函数还能当参数传?还能作为返回值?这听起来有点反直觉。但用了一段时间后,我发现这玩意儿简直是 Kotlin 的灵魂之一。今天我就带你彻底搞懂它。

核心一句话:高阶函数就是「把函数当作普通值来传递」的函数。说白了,函数不再是冷冰冰的代码块,它也能像 Int、String 一样被传来传去。

7.1 函数类型:给函数一个「身份证」

在 Kotlin 里,每个函数都有类型。就像 Int 表示整数,String 表示字符串一样,函数也有自己的类型签名。

// 函数类型的写法:(参数类型) -> 返回类型
val add: (Int, Int) -> Int = { a, b -> a + b }
val greet: (String) -> Unit = { name -> println("Hello, $name") }
val noParam: () -> String = { "Hello" }

我个人习惯把函数类型想象成「函数的模板」。你定义好参数和返回值,后面具体怎么实现,交给 Lambda 或者匿名函数去填充。嗯,这里要注意:Unit 表示没有返回值,不能省略。

小技巧:如果你觉得函数类型写起来太长,可以用 typealias 给它起个别名。比如 typealias IntTransformer = (Int) -> Int,这样代码会清爽很多。

7.2 Lambda 表达式:最常用的函数字面量

Lambda 表达式,说白了就是一段可以传递的代码块。它用花括号包裹,箭头 -> 左边是参数,右边是函数体。

// 完整写法
val sum = { x: Int, y: Int -> x + y }

// 如果最后一个参数是 Lambda,可以提到括号外面
listOf(1, 2, 3).forEach { println(it) }

// 只有一个参数时,可以用 it 代替
listOf(1, 2, 3).map { it * 2 }

我记得刚开始用 Kotlin 时,看到 it 这个隐式参数总觉得有点玄乎。其实它就是 Lambda 只有一个参数时的默认名字。你想想看,如果参数多了,就不能用 it 了,得自己命名。

避坑指南:我曾经在嵌套 Lambda 时滥用 it,结果代码读起来像天书。后来我定了个规矩:Lambda 超过一层嵌套,或者参数含义不明确时,坚决不用 it,老老实实给参数起名。

7.3 匿名函数:另一种函数字面量

匿名函数和 Lambda 很像,但写法不同。它保留了 fun 关键字,但没有函数名。

// Lambda 写法
val multiply1 = { x: Int, y: Int -> x * y }

// 匿名函数写法
val multiply2 = fun(x: Int, y: Int): Int {
    return x * y
}

// 匿名函数的好处:可以有多个 return 点
val check = fun(x: Int): String {
    if (x < 0) return "negative"
    if (x == 0) return "zero"
    return "positive"
}

你可能会问:有 Lambda 了,为什么还要匿名函数?原因很简单:Lambda 里的 return 会直接返回外层函数(这叫非局部返回),而匿名函数的 return 只返回自身。这个区别在写内联函数时特别重要。

7.4 内联函数:性能优化的小秘密

高阶函数虽然好用,但有个问题:每次传递 Lambda 都会创建匿名内部类对象,频繁调用时会有性能开销。内联函数就是来解决这个问题的。

// 加上 inline 关键字
inline fun measureTime(block: () -> Unit) {
    val start = System.nanoTime()
    block()
    val end = System.nanoTime()
    println("耗时:${end - start} ns")
}

// 调用时,编译器会把 block 的代码直接复制到调用处
measureTime {
    Thread.sleep(100)
}

说白了,inline 就是让编译器把函数体「粘贴」到调用处,省去了函数调用的开销。但要注意:不是所有函数都适合内联。如果 Lambda 体很大,内联反而会让字节码膨胀。

我的经验:一般只对参数包含 Lambda 的高阶函数加 inline,而且 Lambda 体要比较小。像 forEachmap 这些标准库函数,Kotlin 团队已经帮我们优化好了,直接用就行。

7.5 常用高阶函数:map、filter、forEach

这三个函数是我每天写 Kotlin 都会用到的。它们让集合操作变得像流水线一样优雅。

7.5.1 forEach:遍历每个元素

val list = listOf("Apple", "Banana", "Cherry")
list.forEach { fruit ->
    println("我喜欢吃 $fruit")
}

// 带索引的遍历
list.forEachIndexed { index, fruit ->
    println("第 ${index + 1} 个水果是 $fruit")
}

forEach 是最简单的高阶函数,它不返回任何值,只做遍历。我个人习惯用它来做「副作用」操作,比如打印日志、更新 UI 等。

7.5.2 map:一对一转换

val numbers = listOf(1, 2, 3, 4, 5)
val squared = numbers.map { it * it }
// 结果:[1, 4, 9, 16, 25]

// 也可以转换类型
val strings = numbers.map { "数字:$it" }
// 结果:["数字:1", "数字:2", ...]

map 的核心思想是「一一映射」。输入多少个元素,输出就多少个元素,只是每个元素都经过了转换。你想想看,如果用传统的 for 循环,你得先建一个可变列表,然后循环添加,多麻烦。

7.5.3 filter:按条件筛选

val numbers = listOf(1, 2, 3, 4, 5, 6)
val evens = numbers.filter { it % 2 == 0 }
// 结果:[2, 4, 6]

// 可以链式调用
val result = numbers
    .filter { it > 2 }
    .map { it * 10 }
// 结果:[30, 40, 50, 60]

链式调用是 Kotlin 函数式编程的精髓。我记得在项目中处理用户列表时,经常这样写:先 filter 出活跃用户,再 map 成展示用的 ViewModel 对象,最后用 forEach 更新 UI。一行链式调用,抵得上十几行传统代码。

性能提醒:链式调用虽然优雅,但每次调用都会创建新的集合。如果数据量很大(比如上万条),建议用 Sequence(序列)来优化,它采用惰性求值,不会创建中间集合。

7.6 知识体系总览

下面这张图是我梳理的高阶函数知识体系,你可以把它当作学习地图。

高阶函数 函数类型 Lambda 表达式 匿名函数 内联函数 (inline) 常用高阶函数:map · filter · forEach 链式调用 代码简洁优雅 惰性求值 Sequence 优化 非局部返回 inline 与 return

7.7 实战小技巧:组合使用

光说不练假把式。我们来看一个实际场景:有一个用户列表,需要筛选出成年用户,提取他们的名字,并打印出来。

data class User(val name: String, val age: Int)

val users = listOf(
    User("Alice", 22),
    User("Bob", 17),
    User("Charlie", 25),
    User("David", 16)
)

// 传统写法
for (user in users) {
    if (user.age >= 18) {
        println(user.name)
    }
}

// 高阶函数写法
users
    .filter { it.age >= 18 }
    .map { it.name }
    .forEach { println(it) }

你更喜欢哪种?我个人觉得高阶函数写法更「声明式」——你只需要告诉程序「我要什么」,而不是「怎么去做」。代码读起来就像在念英语:筛选出成年用户,提取名字,然后打印每个名字。

我的习惯:在团队项目中,我会要求大家尽量用高阶函数处理集合,除非有明确的性能瓶颈。因为这样代码更易读、更易维护。新同事接手代码时,看到链式调用比看到一堆 for 循环要舒服得多。

好了,这一章的内容就到这里。高阶函数是 Kotlin 函数式编程的基石,掌握了它,你写代码的姿势会完全不一样。下一章我们会继续深入,看看 Kotlin 里那些更高级的函数式编程技巧。


公众号:蓝海资料掘金营,微信 deep3321