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 一张图看懂五个函数

我画了张结构图,帮你快速建立整体认知。这张图我反复改了好几版,最后发现按「上下文引用方式」和「返回值」两个维度来划分,最清晰。

KTX 五大作用域函数分类 上下文引用方式 this (接收者) it (参数) 返回值 上下文对象本身 Lambda 最后一行 apply this + 返回对象本身 配置对象属性,链式调用 also it + 返回对象本身 附加操作,不影响原对象 run this + 返回Lambda结果 计算并返回新值 let it + 返回Lambda结果 非空判断、转换操作 with this + 返回Lambda结果

你看,applyalso 返回对象本身,适合链式调用;runletwith 返回Lambda结果,适合做转换。而 with 比较特殊,它是个独立函数,不是扩展函数——这个细节后面会讲。

3.3 逐个拆解:用法与实战

3.3.1 apply:配置对象的最佳拍档

applythis 引用上下文,返回对象本身。说白了就是「帮我配置好这个对象,然后还给我」。

// 传统写法
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:悄悄干点额外的事

alsoit 引用上下文,也返回对象本身。和 apply 的区别在于:apply 里你直接写属性名,also 里你得用 it 来指代对象。

// also 典型场景:日志、校验、调试
val user = User("张三", 28).also {
    Log.d("User", "创建用户: ${it.name}")
    // 这里可以做些不影响主流程的事
}

我在项目中遇到过这样的坑:用 apply 做日志,结果不小心改了对象属性。后来改用 also,因为 it 提醒我「这是附加操作,别乱改」。嗯,这个习惯我一直保留着。

3.3.3 let:非空判断的利器

letit 引用上下文,返回Lambda最后一行。最经典的用法就是配合安全调用操作符 ?.

// 传统非空判断
if (user != null) {
    process(user.name)
}

// let 写法
user?.let {
    process(it.name)
}

我曾经踩过的坑:let 里返回了 Unit,结果外面拿到的值不是预期的。记住:let 返回的是Lambda最后一行的值,如果你不需要返回值,显式写 Unit 或者用 also 代替。

3.3.4 run:既能配置又能计算

runthis 引用上下文,返回Lambda结果。它有点像 applylet 的混合体——用 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
AlertDialog.Builder(this).apply {
    setTitle("提示")
    setMessage("确定删除?")
    setPositiveButton("确定") { _, _ -> }
}.show()
配置完直接 show,链式调用
also
AlertDialog.Builder(this).also {
    it.setTitle("提示")
    it.setMessage("确定删除?")
}.show()
需要写 it,适合附加操作
let
AlertDialog.Builder(this).let {
    it.setTitle("提示")
    it.setMessage("确定删除?")
    it.create()
}.show()
返回 Dialog 对象,再 show
run
AlertDialog.Builder(this).run {
    setTitle("提示")
    setMessage("确定删除?")
    create()
}.show()
this 访问,返回 create() 结果
with
with(AlertDialog.Builder(this)) {
    setTitle("提示")
    setMessage("确定删除?")
    create()
}.show()
参数传入,不是扩展调用

3.5 选型指南:什么时候用哪个?

我总结了一个简单的决策流程,你照着走基本不会错:

  1. 需要返回对象本身? → 用 apply(this)或 also(it)
  2. 需要返回计算结果? → 用 run(this)或 let(it)
  3. 对象是现成的? → 用 with(this)
  4. 需要非空判断? → 用 let(配合 ?.
  5. 配置属性为主? → 用 apply(最直观)

个人偏好:我80%的场景用 applylet。前者搞定配置,后者搞定非空和转换。剩下20%用 runalsowith 用得最少,但偶尔遇到现成对象需要批量操作时,它比 apply 更自然。

3.6 避坑指南

最后分享几个我踩过的坑:

  • 不要嵌套使用:我曾经在 apply 里又套了一个 let,结果 this 指向混乱,调试了半天。如果必须嵌套,用 also 或显式命名参数。
  • 注意 this 的歧义:applyrun 里,this 指向的是上下文对象,不是外部类。想访问外部类的成员,得用 this@OuterClass
  • 返回值别搞错:我见过有人用 apply 想返回计算结果,结果返回了对象本身。记住:applyalso 返回对象,runletwith 返回Lambda结果。

这五个函数用好了,代码会变得非常简洁优雅。但别为了用而用——如果一行代码能搞定,就别硬套作用域函数。毕竟,可读性才是第一位的。

公众号:蓝海资料掘金营,微信 deep3321