Kotlin 五大作用域函数详解|let/run/apply/also/with 选型指南+实战避坑
📖 前言
作用域函数是 Kotlin 最高频、最核心、最容易混淆的语法特性,也是 Android 开发、Compose 开发、面试必考知识点。
Kotlin 提供了 5 个顶级作用域函数:let、run、apply、also、with 。它们的核心作用一致:临时开启一个作用域,简化对象调用、消除冗余代码、优雅处理空安全、实现链式编程。
但绝大多数开发者只会无脑用 let 和 apply,不会精准选型、分不清返回值、不懂嵌套规则,导致代码不规范、埋下隐性 Bug。
本文基于 Kotlin 官方规范 ,从零拆解五大函数的底层差异、适用场景、实战写法、避坑要点、面试考点,读完彻底告别死记硬背,开发时一眼选型。
📊 一、核心总览对照表
五大作用域函数的唯一区别只有两个维度:内部引用方式 + 返回值。
| 函数 | 内部对象引用 | 返回值 | 是否扩展函数 | 核心用途(一句话总结) |
|---|---|---|---|---|
| let | it | Lambda 结果 | 是 | 空安全处理、对象转换、值计算 |
| run | this | Lambda 结果 | 是 | 对象内部批量逻辑 + 结果返回 |
| apply | this | 对象自身 | 是 | 对象初始化、属性配置(Builder 模式) |
| also | it | 对象自身 | 是 | 附加日志、埋点、无侵入副作用 |
| with | this | Lambda 结果 | 否 | 已有对象批量操作,精简代码 |
💡 终极选型口诀
要空安全、要返回新值 → let
要配置对象、链式初始化 → apply
要打印日志、附加操作、不改动对象 → also
对象内批量计算、返回结果 → run
已有非空对象,批量调用方法 → with
二、逐函数深度详解(实战+源码逻辑)
2.1 let:空安全专属、值转换神器
核心特征
-
内部通过 it 引用对象,语义清晰,支持重命名
-
返回 Lambda 最后一行结果
-
扩展函数,天然适配可空对象
?.
最佳场景
-
可空对象非空校验与安全执行(项目最高频)
-
对象属性转换、计算出新结果
-
避免重复调用对象、简化代码
实战代码
kotlin
data class User(val id: Int, val name: String)
fun main() {
val user: User? = User(1001, "Kotlin博主")
// 1. 空安全执行:仅非空时执行
user?.let { safeUser ->
println("用户ID:${safeUser.id}")
println("用户名:${safeUser.name}")
}
// 2. 值转换:返回新结果
val userNameLength = user?.let {
it.name.length
} ?: 0
println("名字长度:$userNameLength")
}
优势与避坑
-
✅ 支持变量重命名,多层嵌套不混淆
-
✅ 完美适配空安全,杜绝 NPE
-
❌ 不要用于对象属性赋值(优先 apply)
2.2 apply:对象初始化配置之王
核心特征
-
内部通过 this 引用对象,可省略
this -
返回 对象自身,支持链式调用
-
专为 对象创建+属性批量赋值 设计
最佳场景
-
实体类、控件、Builder 对象初始化
-
链式构建对象,替代 Java 冗余 set 方法
-
批量赋值、配置参数
实战代码
kotlin
data class Student(var name: String = "", var age: Int = 0, var score: Int = 0)
fun main() {
// 一行完成对象创建+批量赋值,极简优雅
val student = Student().apply {
name = "小明"
age = 18
score = 95
}
println(student)
}
优势与避坑
-
✅ 代码极简,Android 控件初始化、弹窗配置首选
-
✅ 返回自身,可无限链式调用
-
❌ 不会返回计算结果,需要结果值请用 run/let
2.3 also:无侵入副作用操作
核心特征
-
内部 it 引用对象
-
返回 对象自身
-
逻辑与 apply 对称,定位为「附加操作」
最佳场景
-
对象创建后打印日志、埋点统计
-
链式调用中插入中间操作,不改动主逻辑
-
数据校验、状态监听等附加副作用
实战代码
kotlin
fun main() {
val list = mutableListOf<Int>().also {
// 附加初始化日志
println("集合开始初始化")
}.apply {
// 核心赋值逻辑
add(1)
add(2)
add(3)
}.also {
// 结束日志打印
println("集合初始化完成,数据:$it")
}
}
核心区别:apply vs also
-
apply :专注 修改对象属性
-
also :专注 观察对象、附加操作,不修改数据
2.4 run:内部逻辑计算+结果返回
核心特征
-
内部 this 引用对象,省略对象名
-
返回 Lambda 执行结果
-
兼具「对象操作」+「结果计算」双重能力
最佳场景
-
基于对象属性做批量计算、逻辑处理
-
初始化对象同时,直接算出结果
-
简化多处调用同一对象的冗余代码
实战代码
kotlin
data class Order(val price: Int, val count: Int)
fun main() {
val order = Order(10, 5)
// 批量计算并返回结果
val totalPrice = order.run {
println("单价:$price,数量:$count")
price * count
}
println("总价:$totalPrice")
}
2.5 with:已有对象批量精简操作
核心特征
-
非扩展函数,传入对象作为参数
-
内部 this 引用对象
-
返回 Lambda 结果
最佳场景
针对已经存在、非空的对象,批量调用方法、访问属性,彻底省略对象名。
实战代码
kotlin
fun main() {
val sb = StringBuilder()
// 已有对象,批量操作,极简代码
val result = with(sb) {
append("Kotlin ")
append("作用域函数 ")
append("教程")
toString()
}
println(result)
}
with & run 区别(面试高频)
-
run :扩展函数,支持空安全调用
?.run -
with :普通函数,不支持空安全,必须确保对象非空
三、高频两两对比(彻底告别混淆)
3.1 let vs run
-
相同:都返回 Lambda 结果
-
不同:let 用 it(适合空安全、嵌套清晰);run 用 this(适合内部批量逻辑)
3.2 apply vs also
-
相同:都返回对象自身,支持链式
-
不同:apply 修改配置对象;also 观察附加操作
3.3 run vs with
-
相同:内部 this、返回 Lambda 结果、批量操作对象
-
不同:run 可空安全;with 仅用于确定非空对象
四、企业级最佳实践规范
-
空安全判断一律用 let,禁止手动 if != null 嵌套
-
对象初始化、控件配置一律用 apply,替代零散赋值
-
日志、埋点、数据观察用 also,不侵入业务逻辑
-
对象内部计算、多逻辑聚合用 run
-
已存在确定非空对象的批量操作用 with
-
禁止滥用
!!强转,配合 let/run 实现优雅空安全 -
链式组合:
创建对象.apply{}.also{}是项目标准写法
五、面试高频问答总结
Q1:五大作用域函数本质区别?
只有两点不同:内部引用方式(it/this) 、返回值(自身/结果),其余逻辑完全一致。
Q2:apply 和 also 怎么选?
改属性用 apply,打日志/观察用 also。
Q3:run 和 with 区别?
run 是扩展函数,支持空安全;with 是普通函数,必须非空。
Q4:let 为什么适合空安全?
扩展函数 + 可空调用语法 ?.let,仅非空时执行 Lambda,天然杜绝 NPE。