Kotlin 中作用域函数 let、with、run、also、apply 的核心使用指南

345 阅读4分钟

以下是 Kotlin 中作用域函数 letwithrunalsoapply 的核心使用指南,结合其特性、适用场景及代码示例整理而成:


🧩 ​一、作用域函数核心对比

函数上下文对象引用返回值典型场景
​**let**​itLambda 结果空安全操作、数据转换、链式处理中间结果
​**run**​thisLambda 结果对象配置与计算混合、替代 with(扩展函数形式)、链式空安全处理
​**with**​thisLambda 结果集中操作非空对象的多属性/方法(需显式传入对象)
​**apply**​this对象本身对象初始化配置(类似 Builder 模式)、多属性设置
​**also**​it对象本身附加操作(如日志、验证)、链式调用中插入副作用

💡 ​选择关键​:

  • 需返回对象本身 → apply/also;需返回计算结果 → let/run/with
  • 上下文引用偏好 → thisrun/with/apply)更简洁;itlet/also)避免命名冲突。
  • 空安全 → ?.let?.run 优先。

⚙️ ​二、分函数详解与示例

1. ​**let:空安全转换与作用域隔离**​

  • 场景​:处理可空对象、数据转换、链式操作中间处理。

  • 示例​:

    val name: String? = "Kotlin"
    val length = name?.let { 
        println("Processing: $it")  // 输出:Processing: Kotlin
        it.length                   // 返回结果
    } ?: 0                         // 提供默认值
    

    典型链式转换​:

    val numbers = listOf(1, 2, 3)
    val doubled = numbers
        .filter { it > 1 }
        .let { it.map { num -> num * 2 } }  // [4, 6]
    

2. ​**run:对象配置与计算混合**​

  • 场景​:对象初始化+结果计算、替代 with(扩展函数形式)、临时作用域创建。

  • 示例​:

    // 扩展函数形式(配置对象并返回结果)
    val personInfo = Person("Alice", 25).run {
        name = "Bob"           // 直接修改属性(this 可省略)
        age += 1
        "Name: $name, Age: $age"  // 返回字符串
    }  // 输出:Name: Bob, Age: 26
    
    // 非扩展形式(替代临时变量)
    val fullName = run {
        val firstName = "John"
        val lastName = "Doe"
        "$firstName $lastName"     // 返回拼接结果
    }
    

3. ​**with:集中操作非空对象**​

  • 场景​:对同一对象执行多步操作(无需重复写对象名)。

  • 示例​:

    val person = Person("Bob", 25)
    val info = with(person) {
        age = 26                   // 直接访问属性
        "Name: $name, Age: $age"   // 返回结果
    }
    

4. ​**apply:对象初始化配置**​

  • 场景​:构建对象时批量设置属性(返回自身,支持链式)。

  • 示例​:

    val person = Person().apply {
        name = "Charlie"
        age = 28
        address = "Paris"
    }  // 返回已配置的 Person 对象
    

5. ​**also:链式调用附加操作**​

  • 场景​:日志记录、数据验证等副作用操作(不影响对象本身)。

  • 示例​:

    mutableListOf(1, 2, 3)
        .also { println("初始列表: $it") }  // 输出:初始列表: [1, 2, 3]
        .add(4)
    

🛠️ ​三、进阶技巧与避坑指南

  1. 链式组合​:

    • apply + also:初始化后记录日志

      val file = File("data.txt").apply { 
          createNewFile() 
      }.also { println("创建文件: ${it.path}") }
      
    • let + run:空安全转换后继续操作

      user?.let { it.validate() }?.run { process(this) }
      
  2. 空安全优先策略​:

    • 可空对象操作 → ?.let {} ?: default
    • 避免 apply/also 在可空对象上直接调用(需手动判空)。
  3. 性能优化​:

    • 所有作用域函数均为 inline,无运行时开销(编译期内联优化)。 在 Kotlin 中,作用域函数(letrunwithapplyalso)的选择流程可基于 ​返回值需求空安全处理上下文引用方式​(this/it)和链式调用需求综合判断。以下是具体选择流程及示例说明:

🔍 ​选择流程与示例

⚙️ ​步骤1:是否需要返回对象本身?​

  • ​ → 进入步骤2
  • ​ → 进入步骤3

示例场景​:初始化对象或执行副作用操作后需继续操作原对象。

// 初始化对象并返回自身 → 选 apply
val button = Button().apply {
    text = "Submit"
    color = Color.BLUE
} // 返回 Button 对象

// 执行日志后返回原对象 → 选 also
val list = mutableListOf(1, 2).also {
    println("Initial list: $it") // 输出日志
}.add(3) // 继续操作原对象

⚙️ ​步骤2:是否需要隐式接收者(this)?​

  • ​ → ​**apply**​(对象配置)
  • ​ → ​**also**​(附加操作,用 it 引用)

示例对比​:

// apply:用 this 配置属性(简洁)
val config = Config().apply {
    host = "localhost" // 等价于 this.host
    port = 8080
}

// also:用 it 执行日志(避免命名冲突)
val user = User("Alice").also {
    println("Created user: ${it.name}") // it 显式引用
}

⚙️ ​步骤3:是否需要处理可空对象?​

  • ​ → ​**letrun**​(支持 ?. 安全调用)
  • ​ → 进入步骤4

示例场景​:

// let:处理可空字符串
val name: String? = "Kotlin"
val length = name?.let { 
    println("Processing: $it") 
    it.length // 返回长度
} ?: 0 // 提供默认值

// run:可空对象的多步操作
val user: User? = getUser()
user?.run { 
    // this 引用对象
    updateProfile(this) 
    getProfileInfo() // 返回结果
}

⚙️ ​步骤4:是否需要链式调用或扩展函数形式?​

  • ​ → ​**run**​(扩展函数,支持链式)
  • ​ → ​**with**​(显式传入对象)

示例对比​:

// run:链式转换数据
val result = dataSource.fetchData()
    .run { 
        filter { it.isValid } // 操作数据
        convertToModel() 
    } // 返回模型

// with:集中操作非空对象
val list = mutableListOf("A", "B")
val size = with(list) {
    add("C")
    remove("A")
    size // 返回结果
}

⚠️ ​注意事项

  1. 空安全优先​:

    • 可空对象操作避免直接使用 apply/also(需手动判空),优先选 ?.let?.run
    • 错误示例:nullableObj.apply { ... }(可能抛 NPE)。
  2. 组合使用​:

    • apply + also:初始化后记录日志

      val file = File("data.txt").apply { 
          createNewFile() 
      }.also { println("Created: ${it.path}") }
      
    • let + run:空安全转换后继续操作

      user?.let { it.validate() }?.run { process(this) }
      
  3. 性能优化​:
    所有作用域函数均为 inline,无运行时开销。


通过以上流程和示例,可快速定位适合当前场景的作用域函数,兼顾代码简洁性、安全性与可读性。实际开发中需结合具体需求灵活选择,避免生搬硬套。


💎 ​四、总结选择流程图

flowchart TD
    A["需返回对象本身?"] 
    A -->|是| B{"需用 this 配置?"}
    B -->|是| C["apply"]
    B -->|否| D["also"]
    A -->|否| E{"需空安全?"}
    E -->|是| F{"需链式?"}
    F -->|是| G["run"]
    F -->|否| H["let"]
    E -->|否| I{"需扩展函数?"}
    I -->|是| J["run"]
    I -->|否| K["with"]

通过明确操作目标​(配置、转换、副作用)和上下文需求​(空安全、链式、作用域隔离),可精准选用最合适的函数。


整体感觉还是很复杂,记不住!!!

基于这篇详尽的 Kotlin 作用域函数指南,以下是 ​极简记忆口诀​ 和 ​核心区分逻辑,帮你快速掌握精髓:


🧠 一分钟记忆口诀

"配自己用 apply,加操作选 also"
"空安全找 let,计算混用 run"
"with 开会效率高,对象在握不奔波"

🔑 核心区分逻辑(按优先级判断)

  1. 要不要返回对象本身?​

    • ✅ ​​ → 进入 2
    • ❌ ​​ → 进入 3
  2. this还是 it?​

    • 直接调属性 → apply(如 name = "Kotlin"
    • 需对象引用 → also(如 println(it)
  3. 对象可能为空吗?​

    • ✅ ​​ → 用 ?.let?.run
    • ❌ ​​ → 进入 4
  4. 需要链式调用吗?​

    • ✅ ​​ → run(扩展函数形式)
    • ❌ ​​ → with(集中操作非空对象)

⚡️ 高频场景速查表

场景推荐函数示例片段
初始化对象applyTextView().apply { text = "Hi" }
链式调用中打印日志alsolist.also { log(it) }.add(item)
可空对象转换?.letname?.let { it.uppercase() }
对象计算+属性修改runval result = obj.run { update(); getValue() }
集中操作非空对象withwith(list) { add("A"); size }

💡 避坑提醒

  • 空安全第一​:可空对象别直接用 apply/also(会 NPE),加 ?.或改用 let/run
  • 拒绝嵌套地狱​:深层嵌套时换行写,保持可读性
user?.let { u ->
    u.validate().run { 
        process(this) 
    }
}

口诀 + 3 步判断法,实践中形成肌肉记忆后基本 5 秒内可选出正确函数。建议保存速查表,遇到犹豫时快速对照!