简介
在上一节,我们对Kotlin中函数的相关知识有了大致的了解,本章节我们将去了解一些Kotlin中的作用域函数。
目录
- let:处理可空对象,链式操作
- run:对象配置 + 计算返回值
- with:对非空对象执行多个操作
- apply:初始化对象配置
- also:附加操作(如打印日志)
Kotlin 中的作用域函数(Scope Functions)是 let
、run
、with
、apply
、also
,它们可以简化对对象的操作,使代码更简洁
let
:处理可空对象,链式操作
在 Kotlin 中,let
函数通过与 安全调用操作符 ?.
结合使用,优雅地处理可空对象。以下是详细解释:
-
let 处理可空对象的基本原理
-
语法结构:
可空对象?.let{ ... }
-
关键机制:
-
如果对象 非空 ,
?.
会触发let
代码块执行,对象作为参数(默认为it
)传递给 lambda -
如果对象 为空 ,
?.
会跳过let
代码块,整个表达式返回null
,且 lambda 中的代码 不会执行kotlinfun main() { val nullableString: String? = "Hello" // 在类型后面声明 ? 为 可空对象 val length = nullableString?.let { println("执行 let: 对象非空, 内容为 $it") // 非空时才会打印 输出: 执行 let: 对象非空, 内容为 Hello it.length // 返回长度 (最后表达式的结果) } println("字符串长度: $length") // 输出: 字符串长度: 5 // 对象为空的时候 val nullString: String? = null val nullResult = nullString?.let { println("因为对象为空,这里不会执行") // 被跳过 it.length } println("空对象的结果是: $nullResult") // 输出: 空对象的结果是: null }
-
-
-
如果省略安全调用符
?.
(错误示范)直接调用
let
(不加?.
) 可能会导致空指针异常(NPE)kotlinfun main() { val str: String? = null str.let { // 编译警告:此处可能抛出 NPE! println(it!!.length) // 如果 str 为 null, } } /** * Exception in thread "main" java.lang.NullPointerException * at MainKt.main(main.kt:4) * at MainKt.main(main.kt) * */
所以必须使用
?.let
处理可空对象 -
链式操作与默认值处理
结合 Elvis 操作符
?:
,可以为let
的返回结果提供默认值:kotlinfun main() { val input: String? = null val processed = input?.let { it.uppercase() // 非空时转换成大写 } ?: "DEFAULT" // 如何 input 为 null 返回 'DEFAULT' println(processed) // 输出: DEFAULT }
-
经典使用场景
-
避免空检查嵌套
kotlindata class User(val name: String = "") fun main() { val user = User("NPC") // 传统空检查(繁琐) if (user != null) { if (user.name != null) { println(user.name.length) // 输出: 3 } } // 使用 ?.let(简洁) user?.name?.let { println(it.length) } // 输出: 3 }
-
数据转换
kotlinfun main() { val number: Int? = "123".toIntOrNull() println(number) // 输出: 123 val squared = number?.let { it * it } // 非空时计算平方,否则返回 null println(squared) // 输出: 15129 }
-
副作用操作(如打印日志)
kotlinfun main() { val data = "Hello Kotlin" data?.let { println("处理数据: $it") } }
-
run
:对象配置 + 计算返回值
在 Kotlin 中,run
是一个灵活的作用域函数,它有两种形式:扩展函数 和非扩展函数 。它的核心用途是在对象的上下文中执行代码块 ,并返回 lambda 表达式的结果 。以下是 run
的详细讲解
-
run 的两种形式
-
形式1: 拓展函数(对象引用)
kotlin对象.run { // 代码块:通过 this 访问对象 // 最后一行作为返回值 }
-
上下文: 代码内使用
this
引用对象(可省略) -
返回值: lambda 的最后一行结果
-
-
形式2: 非拓展函数(独立作用域)
kotlinrun { // 独立代码块(无需对象) // 最后一行作为返回值 }
- 用途: 创建一个临时作用域,避免变量污染外部环境
-
-
示例代码
-
扩展函数(操作对象并返回结果)
在
Car
对象上下文中配置属性,并返回一个描述状态的字符串。kotlindata class Car(var speed: Int = 0, var isEngineOn: Boolean = false) fun main() { val carStatus = Car().run { this.speed = 100 // 直接访问属性(this 可省略) isEngineOn = true // 修改对象状态 "车速: $speed km/h,引擎状态: ${if (isEngineOn) "开启" else "关闭"}" // 返回字符串 } println(carStatus) // 输出: 车速: 100 km/h,引擎状态: 开启 }
-
处理可空对象(结合空安全调用?.run)
仅在对象非空时执行代码块,避免空指针异常。
kotlinfun printLengthIfNotNull(input: String?) { input?.run { println("字符串内容: $this, 长度: $length") // this 指代 input 对象 } ?: println("输入为空") } fun main() { printLengthIfNotNull("Hello, Kotlin") // 输出: 字符串内容: Hello, Kotlin, 长度: 13 printLengthIfNotNull(null) // 输出: 输入为空 }
-
非扩展形式(独立作用域)
封装临时计算逻辑,避免变量
a
和b
泄漏到外部作用域。kotlinfun main() { val result = run { val a = 10 val b = 20 a + b // 返回计算结果 } println("计算结果:$result") // 输出:计算结果:30 }
-
-
高级用法
-
链式调用多个
run
kotlinfun main() { val message = "Kotlin" .run { uppercase() } // 转换为大写 .run { "Message: $this" } // 添加前缀 println(message) // 输出: Message: KOTLIN }
-
与
apply
结合使用kotlindata class Config(var host: String = "", var port: Int = 0) fun main() { val config = Config().apply { host = "127.0.0.1" // 初始化配置 port = 8080 }.run { "服务器地址: $host:$port" // 转换为连接字符串 } println(config) // 输出: 服务器地址: 127.0.0.1:8080 }
-
apply
:初始化对象配置
在 Kotlin 中,apply
是一个常用的作用域函数,专门用于对象的初始化或配置 。它通过简洁的语法让你在一个代码块中完成对对象属性的设置,并最终返回对象本身,以下是 apply
的详细讲解
-
基本语法
kotlinval 对象 = 原始对象.apply { // 在此配置对象的属性或调用方法 this.property = value // this 可省略 method() // ... } // apply 返回原始对象,可以继续操作
-
示例代码
-
初始化对象属性
kotlindata class Person(var name: String = "", var age: Int = 0) fun main() { val person = Person().apply { name = "Alice" // 等价于 this.name = "Alice" age = 30 // 直接访问属性, this 可省略 } println(person) // 输出: Person(name=Alice, age=30) }
-
链式配置多个属性
kotlinclass Car { var brand: String = "" var speed: Int = 0 fun start() { println("$brand 启动,速度: $speed km/h") } } fun main() { var car = Car() .apply { brand = "XiaoMi SU7" } .apply { speed = 200 } .apply { start() } // 输出: XiaoMi SU7 启动,速度: 200 km/h }
-
替代 Builder 模式
kotlin// 传统 Builder 模式 vs apply 简化 class Dialog { var title: String = "" var message: String = "" fun show() { println("显示对话框:$title - $message") } } fun main() { // 传统方式 val dialog1 = Dialog() dialog1.title = "提示" dialog1.message = "欢迎使用 Kotlin" dialog1.show() // 使用 apply(更简洁) val dialog2 = Dialog().apply { title = "提示" message = "欢迎使用 Kotlin" show() // 直接调用方法 } }
-
处理可空对象
kotlinfun configureNullableObject() { val nullableConfig: String? = null nullableConfig?.apply { println("配置非空对象:$this") // 此处不会执行 } ?: println("对象为空") // 输出:对象为空 } fun main() { configureNullableObject() }
-
-
常见误区
-
错误:在
apply
中返回其他值kotlindata class Person (var name: String = "",var age: Int = 0) fun main() { val person = Person().apply { name = "Bob" "这是一个错误示例" // 这行代码无效 } println(person) }
-
正确的做法是: 若需要返回计算结果,应使用
run
:kotlindata class Person (var name: String = "",var age: Int = 0) fun main() { val person = Person().run { name = "Bob" "姓名: $name" } println(person) // 输出: 姓名: Bob }
-
-
高级用法
-
链式调用多个作用域函数
kotlindata class File(var absolutePath: String = "") { fun readText() { // ... 读取文件操作 println("读取文件操作") } } fun createNewFile() { // ... 创建文件操作 println("创建文件操作") } fun main() { File("data.txt") .apply { createNewFile() } .also { println("文件路径: ${it.absolutePath}") } .readText() }
-
结合
takeIf
过滤条件kotlindata class Person(var name: String = "", var age: Int = 0) fun main() { val validPerson = Person().apply { name = "Charlie" age = 25 }.takeIf { it.age >= 18 } // 只保留成年人 println(validPerson) // 输出:Person(name=Charlie, age=25) }
-
with
:对非空对象执行多个操作
在 Kotlin 中,with
是一个作用域函数,用于对已有对象执行多个操作 ,它通过将对象作为上下文(this
)传递到代码块中,让代码更集中、更易读。以下是 with
的详细讲解
-
基本语法
kotlinval 结果 = with(对象) { // 在此直接访问对象的属性和方法(this 可省略) 操作1 操作2 ... 最后一行作为返回值 }
-
示例代码
-
批量操作对象属性
kotlindata class User(var name: String = "", var age: Int = 0) fun main() { val user = User("Alice", 25) val info = with(user) { name = "Bob" // 等价于 this.name = "Bob" age += 5 // 修改属性 "更新后:$name, $age 岁" // 返回字符串 } println(info) // 输出:更新后:Bob, 30 岁 println(user) // 输出:User(name=Bob, age=30) }
-
执行计算并返回结果
kotlinclass Rectangle(val width: Int, val height: Int) { fun area() = width * height } fun main() { val rect = Rectangle(10, 20) val result = with(rect) { val perimeter = 2 * (width + height) // 访问属性 "面积: ${area()}, 周长: $perimeter" // 调用方法,返回字符串 } println(result) // 输出:面积: 200, 周长: 60 }
-
处理集合操作
kotlinfun main() { val numbers = listOf(1, 2, 3, 4, 5) val summary = with(numbers) { val sum = sum() val avg = average() "总和: $sum, 平均值: $avg" // 返回统计结果 } println(summary) // 输出:总和: 15, 平均值: 3.0 }
-
-
常见误区
-
错误:对可空对象直接使用
with
kotlindata class Person(var name: String = "", var age: Int = 0) fun main() { val person: Person? = null with(person) { println(name) // 编译报错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Person? } }
-
正确做法
kotlindata class Person(var name: String = "", var age: Int = 0) fun main() { val user: Person? = null with(user ?: return) { // 如果 user 为 null,提前退出 println(name) } }
-
-
经典应用场景
-
集中配置对象属性
kotlindata class Person(var name: String = "", var age: Int = 0) { fun sout() { println("Name: $name, Age: $age") } } fun main() { val person = Person() with(person) { name = "NPC" age = 24 sout() // 输出: Name: NPC, Age: 24 } }
-
数据转换与计算
kotlinfun main() { val list = listOf(1, 2, 3) val squaredSum = with(list) { map { it * it }.sum() } println(squaredSum) // 输出: 14 }
-
简化多步操作
kotlindata class File(var fileName: String ="") { fun createNewFile() { println("创建文件 $fileName") } fun exists(): Boolean { println("判断 $fileName 文件是否存在") return false } fun readText(): String { return "读取文件 $fileName" } } fun main() { val file = File("data.txt") val content = with(file) { if (!exists()) createNewFile() readText().uppercase() } println(content) // 输出: 读取文件 DATA.TXT }
-
also
:附加操作(如打印日志)
在 Kotlin 中,also
是一个作用域函数,专注于执行附加操作(如日志记录、验证、调试) ,同时保留对象本身,并支持链式调用。它不会修改对象,但允许在对象操作流程中插入"副作用"。以下是 also
的详细讲解
-
基本语法
kotlinval 对象 = 原始对象.also { // 通过 it 访问对象,执行附加操作 // 返回原始对象(无论代码块内做了什么) }
-
示例代码
-
打印日志(调试中间状态)
kotlindata class User(var name: String, var age: Int) fun main() { val user = User("Alice", 25) .also { println("初始化后:$it") } // 输出:初始化后:User(name=Alice, age=25) .apply { age += 5 } .also { println("修改年龄后:$it") } // 输出:修改年龄后:User(name=Alice, age=30) println("最终结果:$user") // 输出:最终结果:User(name=Alice, age=30) }
-
数据验证
kotlinfun processOrder(order: Order?) { order?.also { require(it.amount > 0) { "订单金额必须大于 0" } requireNotNull(it.customer) { "订单必须有客户信息" } }?.also { println("开始处理订单:${it.id}") // 验证通过后执行 } } data class Order(val id: String, var amount: Double, var customer: String?) fun main() { val validOrder = Order("123", 100.0, "Alice") processOrder(validOrder) // 输出:开始处理订单:123 val invalidOrder = Order("456", -50.0, null) processOrder(invalidOrder) // 抛出 IllegalArgumentException }
-
链式调用中插入操作
kotlinfun main() { val list = mutableListOf(1, 2, 3) .also { it.add(4) } // 添加元素 .also { it.remove(0) } // 删除第一个元素(实际无 0,此操作为演示) .also { println("当前列表:$it") } // 输出:当前列表:[2, 3, 4] println(list) // 输出:[2, 3, 4] }
-
-
常见误区
-
错误: 在
also
中尝试返回其他值kotlindata class User(var name: String, var age: Int) fun main() { // 错误!also 永远返回原始对象,忽略代码块内的返回值 val user = User("Alice", 25).also { "无效的返回值" // 这行代码无意义 } println(user) // 输出:User(name=Alice, age=25) }
-
正确做法: 若需返回计算结果,应使用
let
或run
kotlindata class User(var name: String, var age: Int) fun main() { val info = User("Alice", 25).let { "${it.name} 的年龄是 ${it.age}" // 返回字符串 } println(info) // 输出:Alice 的年龄是 25 }
-
-
高级用法
-
结合
takeIf
进行条件过滤kotlindata class User(var name: String, var age: Int) fun createUser(name: String?, age: Int): User? { return name?.let { User(it, age) }?.takeIf { it.age >= 18 } // 只保留成年人 ?.also { println("用户创建成功:$it") } // 记录日志 } fun main() { val user = createUser("Bob", 20) // 输出:用户创建成功:User(name=Bob, age=20) println(user) // 输出:User(name=Bob, age=20) }
-
再集合操作中跟踪流程
kotlinfun main() { val numbers = (1..10) .filter { it % 2 == 0 } .also { println("过滤后的偶数:$it") } // 输出:过滤后的偶数:[2, 4, 6, 8, 10] .map { it * it } .also { println("平方结果:$it") } // 输出:平方结果:[4, 16, 36, 64, 100] }
-
核心对比表
函数 | 上下文对象引用 | 返回值 | 典型场景 | 空安全支持 |
---|---|---|---|---|
let |
it |
Lambda结果 | 可空对象处理、数据转换 | 需配合?. (object?.let ) |
run |
this |
Lambda结果 | 对象配置 + 返回结果计算、独立作用域 | 需配合?. (object?.run ) |
apply |
this |
对象本身 | 对象初始化(链式配置属性) | 需配合?. (object?.apply ) |
with |
this |
Lambda结果 | 对已有非空对象批量操作 | 需自行处理空安全 |
also |
it |
对象本身 | 附加操作(日志、验证)、链式调用中的中间操作 | 需配合?. (object?.also ) |
-
上下文对象引用
kotlindata class Car(var speed: Int = 0) { fun accelerate() { speed += 10 } } fun main() { val car = Car().apply { speed = 100 // 直接访问属性(this 可省略) accelerate() // 直接调用方法 } println(car) // 输出:Car(speed=110) }
-
it
(显示引用):let
、also
kotlinfun main() { val list = mutableListOf(1, 2, 3) .also { it.add(4) } // 显式用 it 操作 .let { it.joinToString("-") } // 转换为字符串 println(list) // 输出:1-2-3-4 }
-
返回对象本身:
apply
、also
kotlindata class Button(var text: String = "", var textSize: Float = 0.0f) fun main() { // apply 返回对象本身,适合链式配置 val button = Button().apply { text = "Submit" textSize = 16f } // also 返回对象本身,适合插入附加操作 button.also { println("按钮已配置:$it") } // 输出: 按钮已配置:Button(text=Submit, textSize=16.0) }
-
返回 Lambda 结果:
let
、run
、also
kotlindata class Car(var speed: Int = 0) { fun accelerate() { speed += 10 } } fun main() { // let 返回转换后的数据 val length = "Kotlin".let { it.length } println(length) // 输出: 6 // run 返回计算结果 val area = Car(200).run { speed * 2 } println(area) // 输出: 400 // with 返回处理后的结果 val info = with(Car()) { speed = 100 "车速:$speed km/h" } println(info) // 输出: 车速: 100 km/h }
-
需配合
?.
:let
、run
、apply
、also
kotlinfun main() { val nullableString: String? = null nullableString?.let { println(it.length) // 非空时执行 } ?: println("字符串为空") nullableString?.apply { println(length) // 非空时执行 } }
-
需自行处理:
with
kotlindata class User(var name: String = "", var age: Int = 0) fun main() { val user: User? = User() user?.let { with(it) { // 确保非空后使用 with name = "Alice" age = 30 } } println(user) // 输出: User(name=Alice, age=30) }
选择指南
- 是否需要返回对象本身?
- 是 →
apply
或also
(根据是否需要显式it
)。 - 否 →
let
、run
、with
(根据是否需要隐式this
)。
- 是 →
- 是否需要处理可空对象?
- 是 →
?.let
、?.run
、?.apply
、?.also
。 - 否 →
with
或直接使用其他函数。
- 是 →
- 是否需要链式调用中的中间操作?
- 是 →
also
(如记录日志)。 - 否 → 根据返回值需求选择。
- 是 →
混合使用示例
kotlin
// 链式调用:初始化、验证、转换
data class Product(var name: String = "", var price: Double = 0.0)
fun main() {
val productInfo = Product()
.apply {
name = "手机"
price = 2999.0
}
.also {
require(it.price > 0) { "价格必须大于 0" }
println("商品已初始化:$it")
}
.let {
"${it.name} 价格:${it.price} 元" // 返回字符串
}
println(productInfo) // 输出:手机 价格:2999.0 元
}