跳到主要内容

Kotlin 作用域函数实战:用 apply/let/run/also 告别临时变量地狱

· 阅读需 6 分钟
重庆 Kotlin User Group

你有没有写过这样的代码?

// 场景:创建一个用户对象并设置各种属性
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 风格的代码。

作用域函数是什么?​

简单来说,作用域函数可以在一个对象的上下文中执行一段代码块。它们的核心区别在于两点:

  1. 上下文对象怎么引用:用 this 还是 it?
  2. 返回值是什么:返回对象本身,还是 lambda 的结果?

先看一张速查表,后面我们再逐个击破:

函数上下文引用返回值典型用途
applythis对象本身对象初始化、配置
letitlambda 结果空安全、数据转换
runthislambda 结果对象配置并计算结果
alsoit对象本身附加操作、日志记录
withthislambda 结果集中操作同一对象(非扩展)

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 的五个作用域函数各有分工:

  1. apply —— 对象初始化、配置,返回对象本身
  2. let —— 空安全、数据转换,返回 lambda 结果
  3. run —— 配置并计算结果,返回 lambda 结果
  4. also —— 插入副作用,返回对象本身
  5. 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 扩展函数深度探索——从基础语法到高级玩法,让你的工具类"消失"在无形之中。