Kotlin 作用域函数实战:用 apply/let/run/also 告别临时变量地狱
你有没有写过这样的代码?
// 场景:创建一个用户对象并设置各种属性
val user = User()
user.name = "Alice"
user.age = 20
user.email = "alice@example.com"
user.city = "Beijing"
// 或者更糟:为了一个中间结果声明临时变量
val result = computeSomething()
val processed = process(result)
val finalValue = transform(processed)
这种写法能跑,但总让人觉得"不够优雅"。临时变量满天飞,对象初始化代码长得像火车一样。
今天我们来聊聊 Kotlin 的五个作用域函数——apply、let、run、also、with,它们能让你告别临时变量地狱,写出更简洁、更 Kotlin 风格的代码。
作用域函数是什么?
简单来说,作用域函数可以在一个对象的上下文中执行一段代码块。它们的核心区别在于两点:
- 上下文对象怎么引用:用
this还是it? - 返回值是什么:返回对象本身,还是 lambda 的结果?
先看一张速查表,后面我们再逐个击破:
| 函数 | 上下文引用 | 返回值 | 典型用途 |
|---|---|---|---|
apply | this | 对象本身 | 对象初始化、配置 |
let | it | lambda 结果 | 空安全、数据转换 |
run | this | lambda 结果 | 对象配置并计算结果 |
also | it | 对象本身 | 附加操作、日志记录 |
with | this | lambda 结果 | 集中操作同一对象(非扩展) |
apply:对象初始化的好帮手
apply 最常见的场景是对象初始化。它返回对象本身,让你在 lambda 里用 this(可省略)配置属性:
data class User(
var name: String = "",
var age: Int = 0,
var email: String = "",
var city: String = ""
)
// ❌ 传统写法:重复写 user.xxx
val user = User()
user.name = "Alice"
user.age = 20
user.email = "alice@example.com"
user.city = "Beijing"
// ✅ apply 写法:一气呵成
val user = User().apply {
name = "Alice" // 等价于 this.name = "Alice"
age = 20
email = "alice@example.com"
city = "Beijing"
}
实战场景:构建 Android 的 Intent、配置网络请求、初始化 UI 组件等。
// Android 中常见的 Intent 构建
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("user_id", userId)
putExtra("title", title)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
let:空安全和数据转换的利器
let 返回 lambda 的最后一行结果,上下文对象通过 it 引用。它最常用的场景是空安全调用和数据转换:
// ❌ 传统写法:手动判空
val name: String? = getUser()?.name
if (name != null) {
println("Hello, $name")
sendNotification(name)
}
// ✅ let 写法:只在非空时执行
val name: String? = getUser()?.name
name?.let {
println("Hello, $it")
sendNotification(it)
}
数据转换场景:
// 把用户列表转换成用户名集合
val users: List<User> = getUsers()
// ❌ 传统写法
val names = mutableListOf<String>()
for (user in users) {
names.add(user.name)
}
// ✅ let + map
val names = users.let { list ->
list.map { it.name }
}
// 或者直接 map(更简洁)
val names = users.map { it.name }
let 还可以给表达式起个局部名字,避免重复计算:
// 只计算一次耗时函数,结果在 let 块里复用
val result = expensiveComputation().let { value ->
println("计算结果: $value")
process(value)
}
run:配置并返回结果
run 结合了 apply(用 this 访问对象)和 let(返回 lambda 结果)的特点。适合配置对象并计算返回值的场景:
data class Config(var timeout: Int = 3000, var retryCount: Int = 3)
// 配置并返回计算结果
val timeoutInSeconds: Int = Config().run {
timeout = 5000
retryCount = 5
timeout / 1000 // 返回最后一行的结果
}
println(timeoutInSeconds) // 5
run 也可以作为独立函数使用(不依赖对象):
// 初始化一组相关变量
val (host, port, path) = run {
val config = loadConfig()
Triple(config.host, config.port, config.path)
}
also:附加操作的副作用
also 和 let 很像,区别在于它返回对象本身,而不是 lambda 结果。适合在链式调用中插入日志、调试等副作用:
val numbers = mutableListOf(1, 2, 3)
.also { println("初始列表: $it") }
.apply { add(4) }
.also { println("添加后: $it") }
.also { println("最终列表: $it") }
// 输出:
// 初始列表: [1, 2, 3]
// 添加后: [1, 2, 3, 4]
// 最终列表: [1, 2, 3, 4]
实战场景:数据校验、日志记录、埋点上报。
fun processOrder(order: Order) {
order
.also { validate(it) }
.also { log.info("处理订单: ${it.id}") }
.also { reportToAnalytics(it) }
.let { submitToBackend(it) }
}
with:集中操作同一对象
with 不是扩展函数,而是普通函数。它适合对同一个对象做多个操作,但不需要链式调用的场景:
val builder = StringBuilder()
// ❌ 重复写 builder.xxx
builder.append("Hello")
builder.append(" ")
builder.append("World")
val result = builder.toString()
// ✅ with 集中操作
val result = with(StringBuilder()) {
append("Hello")
append(" ")
append("World")
toString() // 返回最后一行
}
实战对比:哪个函数最适合?
来看一个综合场景,体会不同函数的选择:
data class HttpClient(
var baseUrl: String = "",
var timeout: Int = 3000,
var headers: Map<String, String> = emptyMap()
)
fun createClient(): HttpClient {
// 场景 1:初始化配置 → 用 apply
val client = HttpClient().apply {
baseUrl = "https://api.example.com"
timeout = 5000
headers = mapOf("Authorization" to "Bearer token")
}
// 场景 2:基于 client 计算新值 → 用 let 或 run
val timeoutInSeconds = client.let { it.timeout / 1000 }
// 或者
val timeoutInSeconds = client.run { timeout / 1000 }
// 场景 3:链式调用中插入日志 → 用 also
return client.also {
println("Client created: ${it.baseUrl}")
}
}
选择原则:
- 需要配置对象并返回对象本身 →
apply - 需要转换对象或做空安全 →
let - 需要配置对象并返回计算结果 →
run - 需要插入副作用(日志等) →
also - 不是扩展函数,对已有对象做操作 →
with
嵌套使用:优雅还是灾难?
作用域函数可以嵌套,但过度嵌套会让代码难以阅读:
// ⚠️ 嵌套过深,不推荐
val result = user?.let { u ->
u.profile?.apply {
name = "Alice"
}?.also {
println("Profile updated: $it")
}
}
// ✅ 拆分成清晰的步骤
val profile = user?.profile?.apply {
name = "Alice"
}
profile?.also { println("Profile updated: $it") }
经验法则:嵌套不超过 2 层,超过就拆分。
小结
Kotlin 的五个作用域函数各有分工:
apply—— 对象初始化、配置,返回对象本身let—— 空安全、数据转换,返回 lambda 结果run—— 配置并计算结果,返回 lambda 结果also—— 插入副作用,返回对象本身with—— 集中操作同一对象,非扩展函数
记住它们的核心区别(this vs it、返回对象 vs 返回结果),你就能在合适的场景选择合适的函数,写出更简洁、更 Kotlin 的代码。
小挑战
试试用作用域函数重构下面这段代码,让它更简洁:
fun processUser(userId: Int): String? {
val user = findUser(userId)
if (user != null) {
val profile = user.getProfile()
if (profile != null) {
profile.lastLogin = System.currentTimeMillis()
println("Updated profile for ${profile.name}")
return profile.toDisplayName()
}
}
return null
}
提示:试试用 let 和 apply 的组合来消除空判断嵌套。
下期预告:Kotlin 扩展函数深度探索——从基础语法到高级玩法,让你的工具类"消失"在无形之中。