第七章:高阶函数——函数也能当参数?没错,Kotlin 就是这么灵活
说实话,我第一次接触高阶函数这个概念时,心里是有点懵的。函数还能当参数传?还能作为返回值?这听起来有点反直觉。但用了一段时间后,我发现这玩意儿简直是 Kotlin 的灵魂之一。今天我就带你彻底搞懂它。
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 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 了,得自己命名。
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 体很大,内联反而会让字节码膨胀。
forEach、map 这些标准库函数,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。一行链式调用,抵得上十几行传统代码。
7.6 知识体系总览
下面这张图是我梳理的高阶函数知识体系,你可以把它当作学习地图。
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) }
你更喜欢哪种?我个人觉得高阶函数写法更「声明式」——你只需要告诉程序「我要什么」,而不是「怎么去做」。代码读起来就像在念英语:筛选出成年用户,提取名字,然后打印每个名字。
好了,这一章的内容就到这里。高阶函数是 Kotlin 函数式编程的基石,掌握了它,你写代码的姿势会完全不一样。下一章我们会继续深入,看看 Kotlin 里那些更高级的函数式编程技巧。