3、核心扩展函数解析:with、apply、let、run、also的KTX化应用
说实话,刚接触Kotlin那会儿,我对这五个函数是有点懵的。with、apply、let、run、also——长得像,用起来又各有门道。后来在项目里写多了,慢慢就摸清了它们的脾气。今天我就把这五个家伙掰开揉碎了讲给你听。
3.1 它们到底在解决什么问题?
先问一个问题:你写Java的时候,有没有遇到过这种情况——反复写同一个对象名,就为了调几个方法?
// Java 写法
TextView tv = findViewById(R.id.tv_title);
tv.setText("Hello");
tv.setTextColor(Color.RED);
tv.setTextSize(16f);
tv.setVisibility(View.VISIBLE);
嗯,每次都要写 tv.,烦不烦?KTX扩展函数就是来解决这个痛点的。它们让你在对象的作用域内直接操作,不用反复点名。
核心思想:这五个函数本质上都是「作用域函数」——在某个对象的上下文中执行代码块。区别在于:上下文对象的引用方式和返回值不同。
3.2 一张图看懂五个函数
我画了张结构图,帮你快速建立整体认知。这张图我反复改了好几版,最后发现按「上下文引用方式」和「返回值」两个维度来划分,最清晰。
你看,apply 和 also 返回对象本身,适合链式调用;run、let、with 返回Lambda结果,适合做转换。而 with 比较特殊,它是个独立函数,不是扩展函数——这个细节后面会讲。
3.3 逐个拆解:用法与实战
3.3.1 apply:配置对象的最佳拍档
apply 用 this 引用上下文,返回对象本身。说白了就是「帮我配置好这个对象,然后还给我」。
// 传统写法
val tv = TextView(context)
tv.text = "Hello"
tv.textSize = 16f
tv.setTextColor(Color.RED)
// apply 写法
val tv = TextView(context).apply {
text = "Hello"
textSize = 16f
setTextColor(Color.RED)
}
我的习惯:初始化View、构建复杂对象、设置属性时,我首选 apply。它让代码从「先创建再设置」变成了「创建时直接配置好」,语义更连贯。
3.3.2 also:悄悄干点额外的事
also 用 it 引用上下文,也返回对象本身。和 apply 的区别在于:apply 里你直接写属性名,also 里你得用 it 来指代对象。
// also 典型场景:日志、校验、调试
val user = User("张三", 28).also {
Log.d("User", "创建用户: ${it.name}")
// 这里可以做些不影响主流程的事
}
我在项目中遇到过这样的坑:用 apply 做日志,结果不小心改了对象属性。后来改用 also,因为 it 提醒我「这是附加操作,别乱改」。嗯,这个习惯我一直保留着。
3.3.3 let:非空判断的利器
let 用 it 引用上下文,返回Lambda最后一行。最经典的用法就是配合安全调用操作符 ?.。
// 传统非空判断
if (user != null) {
process(user.name)
}
// let 写法
user?.let {
process(it.name)
}
我曾经踩过的坑:在 let 里返回了 Unit,结果外面拿到的值不是预期的。记住:let 返回的是Lambda最后一行的值,如果你不需要返回值,显式写 Unit 或者用 also 代替。
3.3.4 run:既能配置又能计算
run 用 this 引用上下文,返回Lambda结果。它有点像 apply 和 let 的混合体——用 this 访问属性,但又能返回计算结果。
// run 的典型用法:配置 + 计算
val width = textView.run {
text = "Hello"
textSize = 16f
// 返回计算结果
paint.measureText(text.toString()).toInt()
}
你想想看,如果我用 apply,最后还得再取一次宽度;用 let,又得写 it。这时候 run 就刚刚好。
3.3.5 with:独立函数,不搞扩展
with 不是扩展函数,它是一个独立的顶层函数。它接收一个对象作为参数,用 this 引用,返回Lambda结果。
// with 的典型场景:多次操作同一个对象
with(textView) {
text = "Hello"
textSize = 16f
setTextColor(Color.RED)
visibility = View.VISIBLE
}
我的建议:如果你已经有一个现成的对象,想对它做一系列操作,用 with。如果你是在创建对象的同时配置它,用 apply。这两个场景容易混淆,我刚开始也分不清。
3.4 实战对比:同一个需求,五种写法
假设我们要创建一个 AlertDialog,设置标题、消息、按钮,最后显示。看看五种函数分别怎么写:
| 函数 | 代码示例 | 特点 |
|---|---|---|
| apply | |
配置完直接 show,链式调用 |
| also | |
需要写 it,适合附加操作 |
| let | |
返回 Dialog 对象,再 show |
| run | |
this 访问,返回 create() 结果 |
| with | |
参数传入,不是扩展调用 |
3.5 选型指南:什么时候用哪个?
我总结了一个简单的决策流程,你照着走基本不会错:
- 需要返回对象本身? → 用
apply(this)或also(it) - 需要返回计算结果? → 用
run(this)或let(it) - 对象是现成的? → 用
with(this) - 需要非空判断? → 用
let(配合?.) - 配置属性为主? → 用
apply(最直观)
个人偏好:我80%的场景用 apply 和 let。前者搞定配置,后者搞定非空和转换。剩下20%用 run 和 also。with 用得最少,但偶尔遇到现成对象需要批量操作时,它比 apply 更自然。
3.6 避坑指南
最后分享几个我踩过的坑:
- 不要嵌套使用:我曾经在
apply里又套了一个let,结果this指向混乱,调试了半天。如果必须嵌套,用also或显式命名参数。 - 注意 this 的歧义:在
apply或run里,this指向的是上下文对象,不是外部类。想访问外部类的成员,得用this@OuterClass。 - 返回值别搞错:我见过有人用
apply想返回计算结果,结果返回了对象本身。记住:apply和also返回对象,run、let、with返回Lambda结果。
这五个函数用好了,代码会变得非常简洁优雅。但别为了用而用——如果一行代码能搞定,就别硬套作用域函数。毕竟,可读性才是第一位的。