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 类型,调用的时候根本分不清哪个是哪个。后来重构时全部改成命名参数调用,代码可读性直接拉满。

⚠️ 避坑指南: 我曾经在团队里遇到过一个问题——默认参数和 Java 互调时会有坑。Kotlin 的默认参数在 Java 里是不生效的,必须显式传参。如果你写的是 SDK 或者库,记得加 @JvmOverloads 注解,这样 Kotlin 编译器会帮你生成对应的重载方法。

2.3 命名参数:让代码自文档化

命名参数说白了就是调用函数时写上参数名。你想想看,如果看到这样的代码:

// 不写命名参数
updateUser("张三", true, false, 25)

// 写命名参数
updateUser(name = "张三", isActive = true, isAdmin = false, age = 25)

哪个更清楚?明显是第二个。我在 Code Review 时经常要求团队成员对布尔值参数使用命名参数,因为 truefalse 单独放在那里,鬼知道它代表什么。

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 的逻辑统一封装了,业务代码只需要关注正常流程。说白了,高阶函数就是帮你把「控制逻辑」和「业务逻辑」解耦。

🎯 核心理解: 高阶函数让代码从「做什么」变成了「怎么做」。你传进去的 Lambda 是「做什么」,高阶函数本身是「怎么做」。

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 对象,性能直接拉满。

⚠️ 避坑指南: 内联函数不是万能的。我曾经在一个大函数上加了 inline,结果编译后的字节码膨胀了好几倍。内联只适合小函数,尤其是那些接收 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 里不是二等公民,它和变量、对象一样,可以被传递、被返回、被操作。掌握了这个思维,你写出来的代码会完全不一样。