Android面试-Kotlin作用域函数

作用域函数

Kotlin 的标准库函数 let、apply、with、run、also 被统称为 作用域函数 (Scope Functions)

以下是从原理、区别到最佳实践的深度解析。


1. 核心原理:为什么会有这些函数?

这五个函数的核心目的是:在一个对象的上下文中执行代码块

它们的底层实现依赖于两个 Kotlin 关键特性:

内联函数 (inline) :编译时代码直接插入调用处,零运行时开销(不会创建额外的 Function 对象)。

带接收者的 Lambda (T.() -> R):允许在 Lambda 内部直接访问对象的 public 成员(即 this),而无需使用对象名。

源码一瞥(以 apply 为例):

复制代码
// public inline fun  T.apply(block: T.() -> Unit): T
public inline fun  T.apply(block: T.() -> Unit): T {
    block() // 调用 Lambda,此时 this 指向调用者 T
    return this // 返回调用者本身
}

2. 五大函数全图谱 (决策矩阵)

区分它们只需要关注两个维度:

  1. 上下文对象怎么引用? (this 还是 it)

  2. 返回值是什么? (上下文对象本身 Context Object 还是 Lambda 的执行结果 Lambda Result)

|-----------|----------|-----------|------------|-----------------------------|
| 函数 | 对象引用 | 返回值 | 是否扩展函数 | 核心语义 (Mental Model) |
| let | it | Lambda 结果 | 是 | 处理并转换 (如果不为 null,则做...) |
| apply | this | 对象本身 | 是 | 配置 (对这个对象进行初始化配置...) |
| with | this | Lambda 结果 | | 分组 (用这个对象做一系列操作...) |
| run | this | Lambda 结果 | 是 | 计算 (配置并计算出一个新值...) |
| also | it | 对象本身 | 是 | 附加操作 (做完这事,顺便做...) |


3. 深度场景解析与最佳实践

3.1 apply ------ 对象配置专家

场景:对象初始化、构建 Intent、配置 View。

特点:返回对象本身,适合链式调用。

代码

复制代码
// Android 经典场景
val intent = Intent(this, TargetActivity::class.java).apply {
    putExtra("param", "value")
    action = "ACTION_VIEW"
}
 
// 动态添加 View
parentView.addView(TextView(context).apply {
    text = "Hello"
    textSize = 14f
    setOnClickListener { /*...*/ }
})
3.2 let ------ 判空与转换利器

场景:配合 ?. 进行非空执行,或者引入局部变量作用域。

特点:引用是 it(避免 this 冲突),返回最后一行。

代码

Kotlin 复制代码
// 1. 替代 if (obj != null)
mUser?.let { user -> // 建议重命名 it 以增加可读性
    updateUI(user)
}

// 2. 转换作用域
val str: String = 123.let { "The number is $it" }
3.3 with ------ 批量操作符

场景 :由于它不是扩展函数,它适合对一个已经存在的对象进行一连串的方法调用,尤其是 ViewBinding。

代码

Kotlin 复制代码
// 它是 fun  with(receiver: T, block: T.() -> R): R
with(binding) {
    tvTitle.text = "Title"
    btnSubmit.setOnClickListener { }
    ivAvatar.load(url)
}
3.4 also ------ 链式调用中的"副作用"

场景:不想打断链式调用,但需要做一些额外的、不影响流程的事情(如打印日志、缓存数据)。

特点:返回对象本身,引用是 it。

代码

Kotlin 复制代码
// 创建目录,如果成功创建则打印日志,最后返回 file 对象
val file = File("xxx").also {
    if (it.mkdirs()) {
        Log.d("File", "Directory created: ${it.absolutePath}")
    }
}

面试考点:apply 和 also 的区别?

  • apply 用 this,适合设置属性。

  • also 用 it,适合将对象作为参数传给外部函数(如 Log),或者在 this 被遮蔽时使用。

3.5 run ------ 复杂的初始化块

场景:run 是 with 和 let 的结合体。它是扩展函数(像 let),但用 this(像 with)。通常用于**"配置一个对象并计算出结果"**。

代码

Kotlin 复制代码
// 假设 StringBuilder 配置完后,我只需要它的 toString 结果
val result = StringBuilder().run {
    append("Start")
    append("End")
    toString() // 返回值是 String,而不是 StringBuilder
}

4. 常见陷阱 (Senior 必知)

4.1 this 的遮蔽 (Shadowing)

在 apply, run, with 中,this 是隐式的。如果外层类也有同名变量,容易搞混。

Kotlin 复制代码
class Activity {
    val context = this
    
    fun setup() {
        val view = TextView(this)
        view.apply {
            // 这里的 context 是 View 的 context 还是 Activity 的?
            // 答案:是 View 的 context (this.context)
            text = "Hello"
        }
    }
}

建议 :如果代码块内对 this 的引用有歧义,或者需要将对象作为参数传递,优先使用 let 或 also (使用 it)

4.2 嵌套地狱

不要过度嵌套作用域函数,否则可读性会呈指数级下降。

Kotlin 复制代码
// BAD
obj?.let { it1 ->
    it1.apply {
        run {
            // 这里的 this 到底是谁?it1 是谁?完全乱套
        }
    }
}

5. 总结速查表 (Cheat Sheet)

|--------------------------------------|-------------------|
| 我想做什么? | 推荐函数 |
| 对象非空检查 (if not null) | obj?.let { ... } |
| 对象初始化/配置 (Setting properties) | obj.apply { ... } |
| 配置对象并做副作用 (Logging/Caching) | obj.also { ... } |
| 对对象的一组成员进行操作 (Grouping calls) | with(obj) { ... } |
| 计算代码块结果 (Configuration + Return) | obj.run { ... } |

面试题

以下是按照难度分级的常见面试题及深度解析。


第一阶段:原理与机制 (Internals)

Q1: 作用域函数会不会带来性能损耗?比如创建额外的对象?
  • 回答不会

  • 深度解析

    • 所有的作用域函数(let, run, with, apply, also)在 Kotlin 标准库中都被标记为 inline

    • 编译期行为 :编译器会将 Lambda 内部的代码逻辑直接拷贝到调用处。

    • 结果:运行时不会创建 Function 对象(避免了匿名内部类的开销),也没有额外的栈帧调用开销。它们在字节码层面等同于普通的过程式代码。

Q2: 既然 with 和 run 功能非常相似(都使用 this,都返回 Lambda 结果),为什么需要两个?
  • 回答 :它们的调用方式适用场景不同。

  • 深度解析

    • run 是扩展函数 (T.run):

      • 支持安全调用 (?.run { ... }),非常适合处理"如果非空则执行并计算结果"的链式调用。
    • with 是普通函数 (with(T)):

      • 支持安全调用(不能写 ?.with)。

      • 它在语义上更像是一个"语句块"或"分组操作",强调"对于这个对象,执行以下一堆操作"。

    • Code Review 建议:如果对象可能为 null,必用 run;如果对象确定的,用 with 阅读感稍好(但在链式调用头部通常用 run 或 apply 更顺手,with 逐渐在减少使用)。


第二阶段:实战与避坑 (Best Practices)

Q3: apply 和 also 都返回对象本身,开发中怎么抉择?(高频)
  • 回答 :看你是要修改它 ,还是使用它

  • 深度解析

    • apply (Context: this) :语义是 "Apply these settings"。用于初始化修改对象的属性。因为用 this,可以直接调用成员方法/属性。

      • :fragment.apply { arguments = bundle }
    • also (Context: it) :语义是 "And also do this"。用于附加操作(Side-effects),通常不修改对象内部状态,而是利用该对象去做别的事(打印日志、存入缓存、notify)。

      • :val file = makeFile().also { Log.d("TAG", "Created ${it.name}") }
    • 杀手级区别 :当发生 this 遮蔽(Shadowing) 时,或者你需要把当前对象作为参数传给另一个函数时,also (用 it) 比 apply (用 this) 更安全、更清晰。

Q4: 请看下面的代码,指出潜在的问题(坑):
Kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private var id: String = "ActivityID"

    fun setupView() {
        val view = View(this)
        view.apply {
            // 问题:这行代码实际上在给谁赋值?
            id = "ViewID" 
        }
    }
}
  • 回答 :这是典型的 this 引用歧义

  • 解析

    • View 也有一个 setId(int) 方法(虽然参数类型不同,但在属性赋值时容易混淆)。即便参数类型对不上,这种代码可读性极差。

    • 如果是 TextView 的 text 属性,或者 context 属性,就更容易出错。在 apply 块内部,this 指向 View。如果 View 没有该属性,才会去查找外部类(Activity)的属性。

    • 修正 :如果遇到重名属性,或者需要明确引用外部类,应使用 this@MainActivity.id。或者改用 also { it.id = ... } 来明确区分。

Q5: 为什么不推荐嵌套使用作用域函数?
  • 回答:会导致 this 和 it 的语义混乱,极大地降低代码可读性。

  • 解决:尽量展平调用,或者使用具体的参数名代替 it(如 user?.let { user -> ... })。


第三阶段:代码阅读题 (Code Prediction)

Q6: 下面这段代码的输出是什么?
Kotlin 复制代码
val str = "Hello"
val result = str.run {
    this + " World"
}.let {
    it.length
}
println(result)
  • 答案:11

  • 解析

    • str.run { ... }:this 是 "Hello"。"Hello" + " World" 返回 "Hello World"。run 返回 Lambda 结果(String)。

    • .let { ... }:it 是 "Hello World"。it.length 是 11。let 返回 Lambda 结果(Int)。

    • result 类型为 Int,值为 11。

Q7: takeIf 和 takeUnless 是作用域函数吗?配合作用域函数怎么用?
  • 回答 :它们不是标准的作用域函数,但是是过滤函数,通常与作用域函数结合使用。

  • 用法

    • takeIf { predicate }:如果满足条件返回对象本身,否则返回 null。

    • 经典面试场景:如何优雅地读取文件?

Kotlin 复制代码
// 只有当文件存在且可读时,才读取内容
val content = File("config.txt")
    .takeIf { it.exists() && it.canRead() }
    ?.readText()
相关推荐
BlueBirdssh1 小时前
fastboot vs adb 的区别
android·adb
imuliuliang1 小时前
Laravel5.x核心特性全解析
android·运维·数据库·nginx
programhelp_1 小时前
Roblox Coding OA 面经分享|题量不小,但整体更偏工程思维
人工智能·算法·面试
JAVA社区1 小时前
Java进阶全套教程(一)—— 数据框架Mybatis详解
java·开发语言·面试·职场和发展·mybatis
王璐WL1 小时前
【C++进阶】多态,坑很多,面试常考!!!
c++·面试
qq_2518364571 小时前
基于java 安卓-RSS阅读系统毕业论文
android·java·开发语言
JAVA社区1 小时前
Java进阶全套教程(八)—— Docker超详细实战详解
java·运维·开发语言·docker·容器·面试·职场和发展
Mahir0811 小时前
Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析
java·后端·spring·缓存·面试·循环依赖·三级缓存
晓梦林11 小时前
cp520靶场学习笔记
android·笔记·学习