2. Kotlin函数与高阶函数:函数定义、默认参数、命名参数、Lambda表达式、高阶函数、内联函数
说实话,Kotlin 的函数设计是我从 Java 转过来后最爽的部分之一。Java 里写个回调要搞匿名内部类,代码又长又臭。Kotlin 直接给你整了一套「函数是一等公民」的玩法,代码写起来那叫一个丝滑。今天我们就来掰扯清楚这些核心概念。
2.1 函数定义:从基础说起
Kotlin 的函数定义很简洁。用 fun 关键字,后面跟函数名、参数、返回类型。我习惯把返回类型写在冒号后面,这样一眼就能看到返回值。
// 标准写法
fun add(a: Int, b: Int): Int {
return a + b
}
// 表达式体(单行函数)
fun add(a: Int, b: Int) = a + b
注意那个表达式体写法。如果函数体只有一行表达式,可以直接用 = 省略花括号和 return。我在项目中经常用这种写法,尤其是写一些简单的工具函数时,代码看起来特别清爽。
2.2 默认参数:少写很多重载
Java 里为了支持不同参数组合,你得写一堆重载方法。Kotlin 直接支持默认参数,省事多了。
fun createUser(name: String, age: Int = 18, city: String = "北京") {
println("用户:$name,年龄:$age,城市:$city")
}
// 调用时可以不传默认参数
createUser("张三") // 年龄18,城市北京
createUser("李四", 25) // 城市北京
createUser("王五", city = "上海") // 年龄用默认值
看到最后那个调用了吗?我可以用命名参数跳过中间的 age,直接给 city 赋值。这在参数很多的时候特别有用。我曾经维护过一个老项目,有个函数有 8 个参数,全是 String 类型,调用的时候根本分不清哪个是哪个。后来重构时全部改成命名参数调用,代码可读性直接拉满。
@JvmOverloads 注解,这样 Kotlin 编译器会帮你生成对应的重载方法。
2.3 命名参数:让代码自文档化
命名参数说白了就是调用函数时写上参数名。你想想看,如果看到这样的代码:
// 不写命名参数
updateUser("张三", true, false, 25)
// 写命名参数
updateUser(name = "张三", isActive = true, isAdmin = false, age = 25)
哪个更清楚?明显是第二个。我在 Code Review 时经常要求团队成员对布尔值参数使用命名参数,因为 true 和 false 单独放在那里,鬼知道它代表什么。
2.4 Lambda 表达式:函数也能当值用
Lambda 是 Kotlin 函数式编程的基石。说白了,它就是一个可以传递的代码块。语法很简单:
// 完整写法
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
// 简化写法(类型推断)
val sum = { x: Int, y: Int -> x + y }
// 调用
println(sum(3, 5)) // 输出 8
Lambda 有几个约定俗成的规则:
- 如果 Lambda 是函数的最后一个参数,可以写在括号外面
- 如果 Lambda 是唯一参数,括号都可以省略
- 单个参数可以用
it代替
// 传统写法
list.filter({ item -> item.length > 3 })
// 简化写法
list.filter { it.length > 3 }
嗯,这里要注意:it 虽然方便,但嵌套 Lambda 时千万别用。我见过有人写出 list.map { it.filter { it > 0 } },两个 it 指代不同对象,看得人头皮发麻。这种时候一定要显式命名参数。
2.5 高阶函数:函数作为参数或返回值
高阶函数就是接收函数作为参数,或者返回一个函数的函数。这是 Kotlin 函数式编程的核心玩法。
// 接收函数作为参数
fun executeOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
// 调用
val result = executeOperation(10, 5) { x, y -> x * y }
println(result) // 输出 50
我在项目中用得最多的高阶函数场景是「模板方法模式」。比如网络请求的通用封装:
fun <T> safeApiCall(call: () -> T): Result<T> {
return try {
Result.success(call())
} catch (e: Exception) {
Result.failure(e)
}
}
// 使用
val userResult = safeApiCall { api.getUser() }
这样就把 try-catch 的逻辑统一封装了,业务代码只需要关注正常流程。说白了,高阶函数就是帮你把「控制逻辑」和「业务逻辑」解耦。
2.6 内联函数:性能优化利器
Lambda 虽然好用,但有个性能问题——每次创建 Lambda 都会生成一个匿名内部类对象。在高频调用的场景下,这会产生大量临时对象,增加 GC 压力。
内联函数就是来解决这个问题的。用 inline 关键字修饰函数后,编译器会把函数体直接复制到调用处,避免了函数调用和对象创建的开销。
inline fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
val end = System.currentTimeMillis()
println("耗时:${end - start}ms")
}
// 调用时,编译器会把代码内联展开
measureTime {
Thread.sleep(100)
}
内联后的效果相当于:
val start = System.currentTimeMillis()
Thread.sleep(100)
val end = System.currentTimeMillis()
println("耗时:${end - start}ms")
没有函数调用,没有 Lambda 对象,性能直接拉满。
另外,内联函数有个限制——它不能访问非公开的成员。因为内联是把代码复制到调用处,如果调用处不在同一个模块,访问私有成员就会出问题。
2.7 实战:组合使用这些特性
最后,我们来看一个综合例子。假设我们要实现一个「带重试机制的网络请求」:
inline fun <T> retry(
maxRetries: Int = 3,
delayMs: Long = 1000L,
block: () -> T
): T {
var lastException: Exception? = null
for (attempt in 1..maxRetries) {
try {
return block()
} catch (e: Exception) {
lastException = e
if (attempt < maxRetries) {
Thread.sleep(delayMs)
}
}
}
throw lastException ?: RuntimeException("重试失败")
}
// 使用
val data = retry(maxRetries = 5, delayMs = 500L) {
api.fetchData()
}
这个函数用到了默认参数、命名参数、Lambda、高阶函数、内联函数。你看,这些特性组合起来,写出来的代码既简洁又强大。
好了,Kotlin 函数这块的内容就讲到这里。记住一句话:函数在 Kotlin 里不是二等公民,它和变量、对象一样,可以被传递、被返回、被操作。掌握了这个思维,你写出来的代码会完全不一样。