Kotlin 中的作用域函数

Kotlin中的let,run,with,apply,also是管理代码的关键工具,本文详细阐述了它们的用途、区别和应用场景。

作用域函数在 Kotlin 中非常有用,可以帮助我们管理代码并编写清晰易读的代码。

什么是作用域函数?

Kotlin 标准库中包含几个函数,其唯一目的是在对象的上下文中执行一段代码块。当我们在对象上调用这样的函数并提供一个 lambda 表达式时,它形成了一个临时作用域。在这个作用域中,我们可以通过对象的属性和函数来访问该对象,而无需使用对象的名称。这些函数被称为作用域函数。Kotlin 中共有五个作用域函数:letrunwithapply 和 also

关于 this 和 it

  • this:在 runwith 和 apply 函数中,我们可以使用 lambda 接收者关键字 this 来引用上下文对象。因此,在它们的 lambda 表达式中,可以像在普通类函数中一样访问对象。在大多数情况下,当访问接收者对象的成员时,我们可以省略 this,从而使代码更简洁。然而,如果省略了 this,很难区分接收者成员和外部对象或函数之间的区别。因此,在主要通过调用其函数或为属性赋值来操作对象成员的 lambda 中,建议将上下文对象作为接收者 (this)。

val adam = Person("Adam").apply {  
  age = 20 // 与 this.age = 20 相同
  city = "London"
}
println(adam)
  • itlet 和 also 函数将上下文对象作为 lambda 参数引用。如果未指定参数名称,则可以使用隐式的默认名称 it 来访问对象。使用 it 比使用 this 更简洁,使用 it 的表达式通常更易读。然而,当调用对象的函数或属性时,不能像使用 this 那样隐式地访问对象。因此,当对象主要作为函数调用的参数时,通过 it 访问上下文对象更好。如果在代码块中使用多个变量,则使用 it 也更好。

fun getRandomInt(): Int {
  return Random.nextInt(100).also {
    writeToLog("getRandomInt() 生成的值为 $it")
  }
}

val i = getRandomInt()
println(i)

使用作用域函数的应用场景

作用域函数可以使代码更加清晰、易读和简洁,这是 Kotlin 语言的主要特点之一。

作用域函数的类型有五种:let、run、with、apply、also

这些函数之间的主要区别有两点:

  •  引用上下文对象的方式(使用 this 或 it 关键字)
  • 返回值(返回上下文对象或 lambda 结果)

T.() 是让lambda表达式里面持有了thisrun函数), (T) 是让lambda表达式里面持有了itlet函数)

Lambda表达式的特点是,最后一行会自动被认为是返回值类型,

作用域函数比较表:

函数上下文对象引用返回值
letitlambda 结果
runthislambda 结果
withthislambda 结果
applythis对象本身
alsoit对象本身

let 函数

  • 上下文对象:作为参数(it
  • 返回值:lambda 结果

使用场景let 函数经常用于处理可空对象以避免空指针异常。可以使用安全调用操作符(?.)结合 let 来进行空安全调用。它仅在非空值时执行代码块。

  • 可以用于在调用链中的结果上调用一个或多个函数。

示例

// 链式调用
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

// 使用 let
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }
    .filter { it > 3 }
    .let { println(it) } // 可以继续添加更多函数调用
  • 空变量检查

var str: String? = null
// processNonNullString(str) // 编译错误:str 可能为空
var length = str?.let {
    println("let() 在 $it 上调用")
    processNonNullString(it) // OK:'it' 在 '?.let { }' 内部不为空
    it.length
}

let源码分析:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
  • inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
  • <T, R> T.let : T代表是要为T而扩展出一个函数名let(任何类型都可以 万能类型.let) R代表Lambda表达式最后一行返回的类型
  • block: (T) -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最一行返回推断的类型
  • : R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个let函数的返回类型就是Boolean

with 函数

  • 上下文对象:作为接收者(this) 
  • 返回值:lambda 结果

使用场景:推荐使用 with 在上下文对象上调用函数,而不提供 lambda 结果。在代码中,我们可以将 with 理解为“对于这个对象,执行以下操作”。

示例

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' 被调用,参数为 $this")
    println("它包含 $size 个元素")
}

run 函数

  •  上下文对象:作为接收者(this
  • 返回值:lambda 结果

使用场景run 在 lambda 中既可以初始化对象,又可以计算返回值。使用 run 我们可以进行空安全调用以及其他计算操作。

示例

   初始化和计算

val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}
  • 链式空检查

val firstName: String? = null
var middleName: String? = null
var lastName: String? = null
middleName = "M "
lastName = "Vasava"
firstName?.run {
    val fullName = this + middleName + lastName
    print(fullName) // 仅打印 M Vasava
}

run源码分析:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
  • <T, R> T.run : T代表是要为T而扩展出一个函数名run(任何类型都可以 万能类型.run) R代表是Lambda表达式最后一行返回的类型
  • block: T.() -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最后一行返回推断的类型
  • : R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个run函数的返回类型就是Boolean
  • T.() 是让lambda表达式里面持有了thisrun函数), (T) 是让lambda表达式里面持有了itlet函数)

apply 函数

  • 上下文对象:作为接收者(this
  • 返回值:对象本身

使用场景:我们建议在不返回值的代码块中使用 apply,主要用于操作接收者对象的成员。最常见的用例是对象配置。我们可以理解这样的调用为“将以下赋值应用于该对象”。

示例1

val adam = Person("Adam").apply {
    name = "Adam"
    age = 20
    city = "London"
}
println(adam)

示例2:

val dialog = AlertDialog.Builder(this).apply {
    setTitle("警告!")
    setMessage("这是一个警告对话框。")
}.create()

also函数

  • 上下文对象:作为参数(it)。
  • 返回值:对象本身。

使用场景:可用于需要引用对象而不是其属性和函数的操作,或者当您不想从外部作用域隐藏 this 引用时。 当在代码中看到also时,可以将其读作“并且还对对象执行以下操作”。

<think>我们正在讨论Kotlin作用域函数:let、run、with、apply、also。它们都是用来在对象的上下文中执行代码块,但各有特点。根据引用内容,我们可以从以下几个方面区分它们: 1. 上下文对象的引用方式(是`this`还是`it`) 2. 返回值(返回代码块的最后一行还是对象本身) 3. 适用场景 下面我们逐一分析: 1. **let** - 上下文对象引用:作为lambda参数(`it`) - 返回值:代码块的最后一行 - 适用场景: - 处理可空对象(配合`?.`安全调用) - 对对象进行转换操作 - 需要明确使用对象作为参数时 - 示例(来自引用[3]): ```kotlin val nameLength = person?.let { it.name.length // 返回长度 } ``` 2. **run** - 有两种形式: - 扩展函数形式:上下文对象是`this`(可省略) - 非扩展函数形式:直接在代码块中执行,无上下文对象 - 返回值:代码块的最后一行 - 适用场景: - 需要执行一个代码块并返回结果 - 对象初始化并计算一些值 - 链式调用中需要执行多个操作 - 示例: ```kotlin val result = person.run { // 这里this指向person println(name) // 直接访问属性 age * 2 // 返回最后一行 } ``` 3. **apply** - 上下文对象引用:`this`(可省略) - 返回值:对象本身 - 适用场景: - 对象配置(设置多个属性) - 初始化对象 - 示例: ```kotlin val person = Person().apply { name = "Alice" age = 30 // 返回Person对象本身 } ``` 4. **also** - 上下文对象引用:作为lambda参数(`it`) - 返回值:对象本身 - 适用场景: - 需要对象作为参数执行额外操作(如打印日志)而不改变对象 - 链式调用中插入附加操作 - 示例: ```kotlin val person = Person().also { println("Created person: ${it.name}") // 打印信息 // 返回Person对象本身 } ``` 5. **with** - 非扩展函数:将对象作为参数传入 - 上下文对象引用:`this`(可省略) - 返回值:代码块的最后一行 - 适用场景: - 对同一个对象执行多个操作(不需要链式调用) - 将对象作为参数进行一系列操作 - 示例: ```kotlin val result = with(person) { println(name) age * 2 // 返回最后一行 } ``` **总结表格**: | 函数 | 上下文对象 | 返回值 | 主要适用场景 | |--------|------------|--------------|-------------------------------------------| | `let` | `it` | 代码块最后一行 | 安全调用、转换操作、参数传递 | | `run` | `this` | 代码块最后一行 | 对象计算、链式调用、无上下文执行代码块 | | `with` | `this` | 代码块最后一行 | 对已有对象执行多个操作(非扩展) | | `apply`| `this` | 对象本身 | 对象初始化、配置属性 | | `also` | `it` | 对象本身 | 附加操作(如日志)、链式调用中不改变对象 | **选择指南**(参考引用[2]): - 需要返回对象本身? - 是:选择`apply`(配置对象)或`also`(附加操作) - 否:进入下一步 - 使用`this`作为上下文? - 是:选择`run`(需要返回值)或`with`(已有对象且不需要扩展) - 否:选择`let`(需要`it`作为参数) **示例场景对比**: 1. **安全调用(可空对象)**:用`let` ```kotlin person?.let { sendEmail(it) } ``` 2. **对象配置**:用`apply` ```kotlin val dialog = AlertDialog.Builder(context).apply { setTitle("Title") setMessage("Message") }.create() ``` 3. **计算属性**:用`run` ```kotlin val ageAfterTenYears = person.run { age + 10 } ``` 4. **附加操作**:用`also` ```kotlin val book = Book().also { println("Book created: $it") } ``` 5. **对已有对象操作**:用`with` ```kotlin with(recyclerView) { layoutManager = LinearLayoutManager(context) adapter = MyAdapter() } ``` 通过以上对比,我们可以根据具体需求选择合适的作用域函数,使代码更加简洁清晰。 §§相关问题§§ 1. 在Kotlin中如何处理嵌套作用域函数的上下文冲突? 2. 为什么`run`函数有两种形式?它们分别适用于什么情况? 3. 如何利用作用域函数优化Kotlin代码的可读性?</think>### Kotlin 作用域函数详解 Kotlin作用域函数(`let`、`run`、`apply`、`also`、`with`)都是在对象上下文中执行代码块的工具,但它们在**上下文对象引用方式**和**返回值**上存在关键差异: | 函数 | 上下文对象 | 返回值 | 典型使用场景 | |---------|------------|--------------|---------------------------------------| | `let` | `it` | 代码块最后一行 | 空安全检查、对象转换、链式调用[^3] | | `run` | `this` | 代码块最后一行 | 对象初始化+计算、链式操作[^1] | | `apply` | `this` | 对象本身 | 对象配置(多属性设置)[^1] | | `also` | `it` | 对象本身 | 附加操作(日志/验证)、链式调用[^2] | | `with` | `this` | 代码块最后一行 | 非空对象的多操作(替代Java点语法)[^1] | --- ### 核心区别详解 #### 1. **`let` vs `run`** ```kotlin // let:使用 it 访问对象,返回最后一行 val length = user?.let { println("Processing ${it.name}") it.name.length // 返回值 } // run:使用 this 访问对象,返回最后一行 val bio = user.run { "$name, $age years old" // 直接访问属性 } ``` - ✅ 选 `let`:需要空安全检查或对象作为参数传递 - ✅ 选 `run`:对象已非空且需直接访问属性[^1] #### 2. **`apply` vs `also`** ```kotlin // apply:配置对象属性,返回对象本身 val button = Button().apply { text = "Submit" isEnabled = false } // 返回配置好的Button // also:附加操作,返回对象本身 val file = File("data.txt").also { require(it.exists()) // 验证文件存在 println("Created: $it") } // 返回原File对象 ``` - ✅ 选 `apply`:初始化对象并设置多个属性 - ✅ 选 `also`:执行与对象相关的附加操作(日志/验证)[^2] #### 3. **`with` 的特殊性** ```kotlin val user = User("Alice", 30) with(user) { println("$name - $age") // 直接访问属性 incrementAge() // 调用方法 } // 无返回值 ``` - ⚠️ 非扩展函数,需显式传入对象参数 - ✅ 适合对**已初始化对象**执行多个操作[^1] --- ### 使用场景决策树 ```mermaid graph TD A[需要操作对象?] --> B{需要空安全检查?} B --> |是| C[使用 let] B --> |否| D{需要返回对象本身?} D --> |是| E{需要附加操作?} E --> |是| F[使用 also] E --> |否| G[使用 apply] D --> |否| H{操作多个属性/方法?} H --> |是| I[使用 with/run] H --> |否| J[直接操作] ``` --- ### 实战示例 #### 场景1:空安全转换(`let`最佳) ```kotlin val imageUrl: String? = getImageUrl() // 安全转换 + 空值处理 val imageView = imageUrl?.let { url -> loadImage(url) // 加载图片 } ?: placeholderImage // 空时使用占位图 ``` #### 场景2:对象初始化(`apply`最佳) ```kotlin val dialog = AlertDialog.Builder(this).apply { setTitle("警告") setMessage("确定删除?") setPositiveButton("确定") { _, _ -> deleteItem() } setNegativeButton("取消", null) }.create() // 链式创建配置好的对话框 ``` #### 场景3:链式操作(`also` + `let`组合) ```kotlin File("data.log") .also { println("Writing to ${it.path}") } // 日志记录 .writeText("Hello, Kotlin!") .let { Files.getOwner(it.toPath()) } // 获取所有者 ?.also { owner -> println("Owner: $owner") } // 打印所有者 ``` --- ### 关键差异总结 1. **上下文访问** - `this` 风格(`run`/`apply`/`with`):直接访问属性和方法 - `it` 风格(`let`/`also`):对象作为参数传递,避免命名冲突 2. **返回值** - 返回对象本身:`apply`/`also` → 适合构建器模式 - 返回计算结果:`let`/`run`/`with` → 适合转换操作 3. **空安全** - 仅 `let` 可直接配合安全调用操作符 `?.` 处理可空对象[^3] > 💡 **经验法则**: > - 配置对象 → `apply` > - 添加副作用 → `also` > - 空安全转换 → `let` > - 计算属性 → `run` > - 集中操作 → `with`
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值