Kotlin Lambda 表达式详解:从基础语法到实战封装

简介

Lambda 表达式是 Kotlin 里非常常见的语法。

集合操作里的 mapfilterforEach,作用域函数里的 letapplyalso,线程、回调、DSL 风格 API,背后都能看到 Lambda。

一句话概括:

Lambda 就是一段没有名字的函数代码,可以赋值给变量,也可以传给另一个函数执行。

普通函数有名字:

kotlin 复制代码
fun add(a: Int, b: Int): Int {
    return a + b
}

Lambda 没有名字:

kotlin 复制代码
{ a: Int, b: Int -> a + b }

Lambda 的重点不是"少写几个字",而是把一段行为当作数据传来传去。

基础语法

Lambda 的基本格式:

kotlin 复制代码
{ 参数列表 -> 函数体 }

示例:

kotlin 复制代码
val add = { a: Int, b: Int ->
    a + b
}

fun main() {
    println(add(10, 20))
}

输出:

text 复制代码
30

-> 左边是参数,右边是执行逻辑。

如果函数体只有一行,可以写成:

kotlin 复制代码
val add = { a: Int, b: Int -> a + b }

Lambda 的类型

Lambda 也有类型。

kotlin 复制代码
val add: (Int, Int) -> Int = { a, b ->
    a + b
}

这里的:

kotlin 复制代码
(Int, Int) -> Int

表示:

text 复制代码
接收两个 Int,返回一个 Int

常见函数类型:

kotlin 复制代码
() -> Unit

无参数、无返回值。

kotlin 复制代码
(String) -> Unit

接收一个 String,无返回值。

kotlin 复制代码
(Int) -> String

接收一个 Int,返回 String

kotlin 复制代码
(String, Int) -> Boolean

接收 StringInt,返回 Boolean

完整示例:

kotlin 复制代码
val printMessage: (String) -> Unit = { message ->
    println(message)
}

val isAdult: (Int) -> Boolean = { age ->
    age >= 18
}

fun main() {
    printMessage("Kotlin Lambda")
    println(isAdult(20))
}

输出:

text 复制代码
Kotlin Lambda
true

类型写左边还是右边

Lambda 的参数类型可以写在左边,也可以写在右边。

写在右边:

kotlin 复制代码
val add = { a: Int, b: Int ->
    a + b
}

写在左边:

kotlin 复制代码
val add: (Int, Int) -> Int = { a, b ->
    a + b
}

两种写法都可以。

如果左边已经声明了函数类型,右边参数就可以省略类型。

kotlin 复制代码
val square: (Int) -> Int = { number ->
    number * number
}

如果左边没有声明类型,右边参数通常要写清楚类型。

kotlin 复制代码
val square = { number: Int ->
    number * number
}

否则编译器不知道 number 是什么类型。

最后一行就是返回值

Lambda 里不需要写 return

最后一行表达式就是返回值。

kotlin 复制代码
val formatName: (String) -> String = { name ->
    val trimmed = name.trim()
    trimmed.uppercase()
}

fun main() {
    println(formatName(" kotlin "))
}

输出:

text 复制代码
KOTLIN

这里的返回值是:

kotlin 复制代码
trimmed.uppercase()

如果最后一行是 println,返回值就是 Unit

kotlin 复制代码
val log: (String) -> Unit = { message ->
    println("日志: $message")
}

it 关键字

当 Lambda 只有一个参数,并且没有显式写参数名时,可以使用 it

kotlin 复制代码
val numbers = listOf(1, 2, 3, 4)

val result = numbers.map {
    it * 2
}

println(result)

输出:

text 复制代码
[2, 4, 6, 8]

上面代码等价于:

kotlin 复制代码
val result = numbers.map { number ->
    number * 2
}

it 适合短逻辑:

kotlin 复制代码
users.filter { it.age >= 18 }

逻辑变复杂时,写出参数名更清楚:

kotlin 复制代码
users.filter { user ->
    user.age >= 18 && user.city == "上海" && user.balance > 1000
}

尾随 Lambda

如果函数的最后一个参数是 Lambda,可以把 Lambda 放到小括号外面。

先定义一个函数:

kotlin 复制代码
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

普通写法:

kotlin 复制代码
val result = calculate(10, 20, { x, y ->
    x + y
})

Kotlin 常用写法:

kotlin 复制代码
val result = calculate(10, 20) { x, y ->
    x + y
}

如果函数只有一个 Lambda 参数,小括号也可以省略。

kotlin 复制代码
fun runTask(task: () -> Unit) {
    task()
}

fun main() {
    runTask {
        println("任务执行")
    }
}

输出:

text 复制代码
任务执行

这就是很多 Kotlin API 看起来像 DSL 的原因。

忽略不用的参数

Lambda 有多个参数,但某些参数用不到,可以使用 _ 忽略。

kotlin 复制代码
val scores = mapOf(
    "张三" to 90,
    "李四" to 86,
    "王五" to 95
)

scores.forEach { _, score ->
    println(score)
}

输出:

text 复制代码
90
86
95

只关心分数,不关心姓名时,_ 比随便起一个变量名更清楚。

Lambda 作为函数参数

Lambda 最常见的用法就是传给高阶函数。

kotlin 复制代码
fun checkAge(age: Int, rule: (Int) -> Boolean): Boolean {
    return rule(age)
}

fun main() {
    val adult = checkAge(20) { age ->
        age >= 18
    }

    val child = checkAge(12) {
        it < 18
    }

    println(adult)
    println(child)
}

输出:

text 复制代码
true
true

checkAge 不固定判断规则,规则由外部传入。

Lambda 作为返回值

函数也可以返回一个 Lambda。

kotlin 复制代码
fun createDiscount(level: String): (Double) -> Double {
    return when (level) {
        "vip" -> { price -> price * 0.8 }
        "svip" -> { price -> price * 0.6 }
        else -> { price -> price }
    }
}

fun main() {
    val vipDiscount = createDiscount("vip")
    val normalDiscount = createDiscount("normal")

    println(vipDiscount(100.0))
    println(normalDiscount(100.0))
}

输出:

text 复制代码
80.0
100.0

这类写法适合做策略选择。先根据条件选出一段逻辑,再在后面执行。

闭包:Lambda 可以记住外部变量

Lambda 可以访问外部变量,这叫闭包。

kotlin 复制代码
fun createCounter(): () -> Int {
    var count = 0

    return {
        count++
        count
    }
}

fun main() {
    val counter = createCounter()

    println(counter())
    println(counter())
    println(counter())
}

输出:

text 复制代码
1
2
3

createCounter 执行结束后,count 并没有丢失。返回出去的 Lambda 仍然能访问它。

再看两个计数器:

kotlin 复制代码
fun main() {
    val counterA = createCounter()
    val counterB = createCounter()

    println(counterA())
    println(counterA())
    println(counterB())
}

输出:

text 复制代码
1
2
1

每次调用 createCounter(),都会产生一份新的 count

Lambda 和匿名函数

Lambda:

kotlin 复制代码
val add = { a: Int, b: Int ->
    a + b
}

匿名函数:

kotlin 复制代码
val add = fun(a: Int, b: Int): Int {
    return a + b
}

两者都没有函数名,都可以赋值给变量。

区别主要在返回值写法:

  • Lambda 最后一行自动返回
  • 匿名函数使用 return
  • 匿名函数的参数和返回类型写得更像普通函数

匿名函数适合返回逻辑比较复杂、需要显式 return 的场景。

return 和标签返回

Lambda 里的 return 容易让代码产生误解。

先看一个常见写法:

kotlin 复制代码
fun main() {
    val result = listOf(1, 2, 3, 4).map {
        if (it == 2) {
            return@map 0
        }
        it * 10
    }

    println(result)
}

输出:

text 复制代码
[10, 0, 30, 40]

return@map 0 的意思是:只从当前 map 的 Lambda 返回一个值,不是结束 main

这里的 0 是当前元素转换后的结果。

map 会把每个元素转换成一个新值,所以 return@map 后面要跟一个返回值。

执行过程可以理解成:

text 复制代码
1 -> 10
2 -> 0
3 -> 30
4 -> 40

所以最终结果是:

text 复制代码
[10, 0, 30, 40]

forEach 里也经常这样写:

kotlin 复制代码
fun main() {
    listOf("A", "B", "C").forEach {
        if (it == "B") {
            return@forEach
        }
        println(it)
    }
}

输出:

text 复制代码
A
C

return@forEach 类似循环里的 continue,跳过当前这次 Lambda。

forEach 只是执行动作,不负责生成新集合,所以 return@forEach 后面不用跟返回值。

隐式标签

return@mapreturn@forEach 里的 mapforEach 不是随便起的名字。

当 Lambda 传给某个函数时,Kotlin 会自动生成一个隐式标签。标签名默认就是接收这个 Lambda 的函数名。

kotlin 复制代码
val result = listOf(1, 2, 3).map {
    if (it == 2) {
        return@map 0
    }
    it * 10
}

这个 Lambda 是传给 map 的,所以可以使用 return@map

kotlin 复制代码
listOf("A", "B", "C").forEach {
    if (it == "B") {
        return@forEach
    }
    println(it)
}

这个 Lambda 是传给 forEach 的,所以可以使用 return@forEach

如果写一个不存在的标签,会直接编译失败。

kotlin 复制代码
listOf(1, 2, 3).forEach {
    return@abc
}

abc 没有定义,也不是当前 Lambda 的隐式标签,所以不能这样写。

自定义标签

标签也可以自定义。

写法是在 Lambda 前面加上标签名:

kotlin 复制代码
listOf(1, 2, 3).forEach myLoop@{
    if (it == 2) {
        return@myLoop
    }
    println(it)
}

输出:

text 复制代码
1
3

这里的 myLoop@ 是自定义标签,所以内部可以写 return@myLoop

隐式标签和自定义标签可以这样区分:

text 复制代码
return@map      使用 map 的隐式标签
return@forEach  使用 forEach 的隐式标签
return@myLoop   使用 myLoop 自定义标签

Lambda 接收者

Lambda 还可以带接收者。

普通 Lambda 类型:

kotlin 复制代码
(String) -> Int

带接收者的 Lambda 类型:

kotlin 复制代码
String.() -> Int

区别在于:带接收者的 Lambda 里面可以直接访问接收者成员。

kotlin 复制代码
val lengthGetter: String.() -> Int = {
    length
}

fun main() {
    println("Kotlin".lengthGetter())
}

输出:

text 复制代码
6

这里的 length 来自 String 对象本身。

apply 就是典型的接收者 Lambda。

kotlin 复制代码
data class ServerConfig(
    var host: String = "",
    var port: Int = 0,
    var debug: Boolean = false
)

fun main() {
    val config = ServerConfig().apply {
        host = "127.0.0.1"
        port = 8080
        debug = true
    }

    println(config)
}

apply 的 Lambda 里,可以直接写 hostportdebug

集合实战:订单统计

准备数据:

kotlin 复制代码
data class Order(
    val id: String,
    val userId: Int,
    val city: String,
    val amount: Double,
    val paid: Boolean
)

val orders = listOf(
    Order("A001", 1, "上海", 120.0, true),
    Order("A002", 2, "北京", 80.0, false),
    Order("A003", 1, "上海", 260.0, true),
    Order("A004", 3, "深圳", 300.0, true),
    Order("A005", 2, "北京", 180.0, true)
)

需求:

  • 只统计已支付订单
  • 按城市分组
  • 统计每个城市的订单数和总金额
  • 按总金额倒序

代码:

kotlin 复制代码
data class CityOrderReport(
    val city: String,
    val count: Int,
    val totalAmount: Double
)

fun buildReport(orders: List<Order>): List<CityOrderReport> {
    return orders
        .filter { it.paid }
        .groupBy { it.city }
        .map { (city, cityOrders) ->
            CityOrderReport(
                city = city,
                count = cityOrders.size,
                totalAmount = cityOrders.sumOf { it.amount }
            )
        }
        .sortedByDescending { it.totalAmount }
}

fun main() {
    val reports = buildReport(orders)

    reports.forEach {
        println("${it.city} 订单数=${it.count}, 金额=${it.totalAmount}")
    }
}

输出:

text 复制代码
上海 订单数=2, 金额=380.0
深圳 订单数=1, 金额=300.0
北京 订单数=1, 金额=180.0

这段代码里每个 Lambda 都只做一件事:

  • filter { it.paid }:过滤已支付订单
  • groupBy { it.city }:按城市分组
  • map { ... }:转换成报表对象
  • sortedByDescending { it.totalAmount }:按总金额排序

实战 Demo:参数校验器

业务参数校验经常会出现一堆 if

可以用 Lambda 把规则抽出来。

kotlin 复制代码
data class RegisterForm(
    val username: String,
    val password: String,
    val age: Int
)

class Validator<T>(private val value: T) {
    private val errors = mutableListOf<String>()

    fun rule(message: String, predicate: (T) -> Boolean): Validator<T> {
        if (!predicate(value)) {
            errors.add(message)
        }
        return this
    }

    fun result(): List<String> {
        return errors
    }
}

fun <T> validate(value: T, block: Validator<T>.() -> Unit): List<String> {
    val validator = Validator(value)
    validator.block()
    return validator.result()
}

fun main() {
    val form = RegisterForm(
        username = "tom",
        password = "123",
        age = 16
    )

    val errors = validate(form) {
        rule("用户名长度不能小于 4") { it.username.length >= 4 }
        rule("密码长度不能小于 6") { it.password.length >= 6 }
        rule("年龄不能小于 18") { it.age >= 18 }
    }

    println(errors)
}

输出:

text 复制代码
[用户名长度不能小于 4, 密码长度不能小于 6, 年龄不能小于 18]

这里有两种 Lambda:

kotlin 复制代码
predicate: (T) -> Boolean

普通 Lambda,用来判断规则是否通过。

kotlin 复制代码
block: Validator<T>.() -> Unit

带接收者的 Lambda,用来写出接近配置的调用方式。

实战 Demo:安全执行

异常处理也适合用 Lambda 封装。

kotlin 复制代码
fun <T> safeRun(defaultValue: T, block: () -> T): T {
    return try {
        block()
    } catch (e: Exception) {
        println("执行失败: ${e.message}")
        defaultValue
    }
}

fun main() {
    val value1 = safeRun(0) {
        "100".toInt()
    }

    val value2 = safeRun(0) {
        "abc".toInt()
    }

    println(value1)
    println(value2)
}

输出:

text 复制代码
执行失败: For input string: "abc"
100
0

safeRun 负责异常兜底,传入的 Lambda 负责具体业务。

实战 Demo:耗时统计

kotlin 复制代码
fun <T> measure(name: String, block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    val cost = System.currentTimeMillis() - start
    println("$name 耗时 ${cost}ms")
    return result
}

fun main() {
    val names = measure("加载用户名单") {
        Thread.sleep(200)
        listOf("张三", "李四", "王五")
    }

    println(names)
}

输出:

text 复制代码
加载用户名单 耗时 200ms
[张三, 李四, 王五]

实际耗时会有少量波动。

实战 Demo:防重复点击

防重复点击的核心是:返回一个带时间判断的新 Lambda。

kotlin 复制代码
fun <T> throttle(intervalMillis: Long, action: (T) -> Unit): (T) -> Unit {
    var lastTime = 0L

    return { value: T ->
        val now = System.currentTimeMillis()
        if (now - lastTime >= intervalMillis) {
            lastTime = now
            action(value)
        }
    }
}

fun main() {
    val submit = throttle<String>(1000) { text ->
        println("提交: $text")
    }

    submit("第一次")
    submit("第二次")

    Thread.sleep(1100)

    submit("第三次")
}

可能输出:

text 复制代码
提交: 第一次
提交: 第三次

第二次调用和第一次距离太近,所以被忽略。

SAM 转换

SAM 是 Single Abstract Method 的缩写,表示只有一个抽象方法的接口。

Java 里很多回调接口都属于 SAM。

例如 Java 常见写法:

java 复制代码
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("run");
    }
}).start();

Kotlin 可以直接写 Lambda:

kotlin 复制代码
Thread {
    println("run")
}.start()

这是因为 Runnable 只有一个抽象方法,Kotlin 可以把 Lambda 转成对应接口对象。

自定义 Kotlin fun interface 也支持这种写法:

kotlin 复制代码
fun interface ClickListener {
    fun onClick(id: Int)
}

fun setClickListener(listener: ClickListener) {
    listener.onClick(100)
}

fun main() {
    setClickListener { id ->
        println("点击 id=$id")
    }
}

输出:

text 复制代码
点击 id=100

Lambda 和 inline

Lambda 传给函数时,可能会生成函数对象。

对于短小、频繁调用的高阶函数,可以使用 inline

kotlin 复制代码
inline fun repeatTimes(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) {
        action(i)
    }
}

fun main() {
    repeatTimes(3) {
        println("第 $it 次")
    }
}

输出:

text 复制代码
第 0 次
第 1 次
第 2 次

inline 会让编译器把函数体展开到调用处,减少部分函数对象开销。

不过 inline 不是越多越好。函数体太大时,内联会让生成代码变多。

常见写法对比

单参数短逻辑

kotlin 复制代码
users.filter { it.age >= 18 }

单参数复杂逻辑

kotlin 复制代码
users.filter { user ->
    user.age >= 18 &&
        user.city == "上海" &&
        user.balance >= 1000
}

多参数 Lambda

kotlin 复制代码
map.forEach { key, value ->
    println("$key = $value")
}

忽略参数

kotlin 复制代码
map.forEach { _, value ->
    println(value)
}

函数引用

kotlin 复制代码
fun isPaid(order: Order): Boolean {
    return order.paid
}

val paidOrders = orders.filter(::isPaid)

如果已有函数刚好能匹配 Lambda 类型,函数引用更干净。

常见注意点

不要把 it 用到看不懂

下面这种写法阅读成本很高:

kotlin 复制代码
orders.map {
    it.copy(id = it.id.trim(), city = it.city.trim())
}.filter {
    it.paid && it.amount > 100
}

拆出参数名会好一些:

kotlin 复制代码
orders.map { order ->
    order.copy(id = order.id.trim(), city = order.city.trim())
}.filter { order ->
    order.paid && order.amount > 100
}

Lambda 太长就提取函数

kotlin 复制代码
fun isValidOrder(order: Order): Boolean {
    return order.paid && order.amount > 100 && order.city.isNotBlank()
}

val result = orders.filter(::isValidOrder)

有名字的函数可以解释业务含义,比一大段 Lambda 更直观。

闭包里修改 var 要谨慎

kotlin 复制代码
var total = 0.0

orders.forEach {
    total += it.amount
}

能写,但更推荐使用集合函数表达累计逻辑:

kotlin 复制代码
val total = orders.sumOf { it.amount }

状态越少,代码越稳。

map 和 forEach 不要混用目的

map 用来转换并产生新集合。

kotlin 复制代码
val names = users.map { it.name }

forEach 用来执行动作。

kotlin 复制代码
users.forEach {
    println(it.name)
}

不要为了打印日志使用 map

总结

Lambda 是一段可以传递的匿名函数代码。

重点掌握这些内容:

  • 基础格式:{ 参数 -> 函数体 }
  • 函数类型:(Int, Int) -> Int
  • 单参数简写:it
  • 最后一行自动作为返回值
  • 尾随 Lambda:最后一个 Lambda 参数可以放到括号外
  • 标签返回:return@mapreturn@forEach
  • 闭包:Lambda 可以访问外部变量
  • 接收者 Lambda:String.() -> Int
  • SAM 转换:Lambda 可以转换成单抽象方法接口
  • 性能优化:合适时使用 inline

Lambda 写得好,代码会更短、更清楚。Lambda 写得太满,代码也会变绕。简单逻辑直接写 Lambda,复杂业务提取成有名字的函数,通常是更稳的写法。

相关推荐
帅次3 小时前
Jetpack Compose 动画实战:animateFloatAsState、AnimatedVisibility 与 graphicsLayer 避坑
android·kotlin·gradle·android jetpack
Kapaseker7 小时前
学吧!Android 全新的嵌入式照片选择器
android·kotlin
Carson带你学Android1 天前
告别复杂的 Gradle 配置!JetBrains Amper 0.10 发布:用 YAML 构建 Kotlin/Android 项目
kotlin·gradle
Meteors.1 天前
Kotlin代码优化
开发语言·kotlin
疏狂难除1 天前
JetBrains IDE插件开发教程(三)——plugin.xml与命令
ide·kotlin·intellij-idea
Refrain_zc2 天前
Android高含金量实战:音频文本 HTML 标签解析 + 段落分组 + 自定义圆角 SpanUI 渲染
kotlin
huaCodeA2 天前
Android面试-Kotlin Coroutines(协程)
android·面试·kotlin
jzlhll1232 天前
android kotlin Flow:distinctUntilChangedBy + stateIn 的坑
android·开发语言·kotlin