Kotlin 作用域函数笔记
Kotlin 作用域函数
作用域函数是 Kotlin 标准库中的一组函数,它们的唯一目的是在某个对象的上下文中执行一段代码块。共有 5 个:let、apply、also、run、with。
终极对比速查表
| 函数 | 上下文引用 | 返回值 | 是否扩展函数 | 核心一句话记忆 | 最适用场景 |
|---|---|---|---|---|---|
let | it(可重命名) | 代码块最后一行 | ✅ | 让它去做转换,或安全判空。 | ?.let 判空执行;将对象 A 转换为结果 B。 |
apply | this(可省略) | 对象本身 | ✅ | 把这些属性应用到我身上。 | 对象的初始化、UI 组件的集中配置。 |
also | it(可重命名) | 对象本身 | ✅ | 原封不动交接,而且顺便做点事。 | 链式调用中的副作用(打日志、埋点、校验)。 |
run | this(可省略) | 代码块最后一行 | ✅ | 在它内部跑一段逻辑并拿结果。 | 复杂对象的构建与计算;初始化并立即产生结果。 |
with | this(可省略) | 代码块最后一行 | ❌(普通函数) | 和它一起,集中调用它的方法。 | 对同一个已存在的对象进行多次方法调用。 |
with 不是扩展函数,对象作为第一个参数传入,因此不适合用于可空对象(无法使用 ?.with)。
各函数详解
let —— 转换 & 判空
// 典型用法 1:安全判空(最常用)
currentUser?.let { user ->
updateUI(user.name)
sendAnalytics(user.id)
}
// 典型用法 2:将对象转换为另一种形式
val length = "Hello".let { str ->
println("字符串是:$str")
str.length // 返回值
}
// length = 5
// 典型用法 3:限制变量作用域,避免污染外部
val result = someHeavyComputation().let { value ->
value * 2 + 1
}apply —— 初始化配置(返回自身)
// 典型用法 1:Android Paint 对象初始化
val paint = Paint().apply {
color = Color.RED
strokeWidth = 5f
style = Paint.Style.STROKE
}
// 典型用法 2:Android View 配置
val button = Button(context).apply {
text = "确认"
textSize = 16f
setOnClickListener { onConfirmClick() }
}
// 典型用法 3:构建请求参数
val config = SdkConfig().apply {
environment = "PROD"
timeout = 3000
retryCount = 3
}also —— 副作用(返回自身,不影响主流程)
// 典型用法 1:链式调用中插入日志
return repository.getUser(id)
.also { Log.d("TAG", "Fetched user: ${it?.id}") }
.also { analytics.track("user_fetched") }
// 典型用法 2:参数校验
fun processData(list: MutableList<String>) = list.also {
require(it.isNotEmpty()) { "列表不能为空" }
}
// 典型用法 3:调试时临时打印中间值
val users = fetchUsers()
.also { println("原始数据: $it") }
.filter { it.isActive }
.also { println("过滤后: $it") }run —— 在对象上下文中执行并返回结果
// 典型用法 1:对象的复杂计算
val displayText = user.run {
if (isVip) "$name VIP" else name
}
// 典型用法 2:可空对象判空 + 计算
val result = nullableObj?.run {
doSomething()
computeResult() // 返回值
}
// 典型用法 3:作为独立的代码块(非扩展函数形式)
val value = run {
val a = computeA()
val b = computeB()
a + b
}with —— 对已有对象多次调用(非扩展函数)
// 典型用法 1:集中操作同一对象
with(binding) {
tvTitle.text = "Hello"
tvSubtitle.text = "World"
btnAction.isVisible = true
}
// 典型用法 2:返回计算结果
val area = with(rectangle) {
width * height
}
// 注意:不适合用于可空对象,改用 run
nullableObj?.run { doSomething() } // 推荐替代 with实战中的"肌肉记忆"
1. 遇到可空类型 ?,首选 let:
currentUser?.let { user ->
updateUI(user.name)
}2. new 一个对象并需要设置属性,无脑选 apply:
val config = SdkConfig().apply {
environment = "PROD"
timeout = 3000
}3. 函数返回前想插一脚(比如打日志),用 also:
return database.getUser().also {
Log.d("SDK", "Fetched user: ${it.id}")
}4. 需要在对象上下文中计算出一个新值,用 run:
val displayString = jsonParser.run {
val parsed = parse(rawJson)
format(parsed)
}5. 对已有对象集中调用多个方法,用 with:
with(textView) {
text = "Kotlin"
textSize = 18f
setTextColor(Color.BLACK)
}选择决策树
需要用作用域函数?
│
├─ 对象可能为 null?
│ ├─ 是 → 需要返回新结果?
│ │ ├─ 是 → let
│ │ └─ 否 → let 或 run
│ └─ 否 ↓
│
├─ 是否要返回对象本身(链式调用)?
│ ├─ 是 → 需要 it 引用?
│ │ ├─ 是 → also(副作用)
│ │ └─ 否 → apply(初始化配置)
│ └─ 否 ↓
│
├─ 对象是否已存在(非新建)?
│ ├─ 是,多次调用其方法 → with
│ └─ 否,需要计算新值 → run终极防混淆口诀
- 要改变/配置自己 →
apply - 要转换出新结果 →
let/run - 要顺便看一眼(不改变业务流) →
also - 要集中调用已有对象的方法 →
with
常见反模式(别这样写)
// 嵌套多层作用域函数,可读性极差
obj.let { it.apply { config() }.also { log(it) } } // 不推荐
// 推荐:拆开写,清晰
obj.apply { config() }
.also { log(it) }
// apply 做转换(apply 返回自身,拿不到转换结果)
val name = user.apply { "Hello, $name" } // 返回的是 user,不是字符串!
// 推荐:用 let 做转换
val greeting = user.let { "Hello, ${it.name}" }
// 对非空对象用 let 做简单调用,没必要
val nonNull = "hello"
nonNull.let { it.uppercase() } // 多此一举
"hello".uppercase() // 直接调用即可