Kotlin作用域函数
let、run、with、apply、also 五大作用域函数深度剖析
学习目标
完成本文学习后,读者应能够在以下认知层级达成对应能力(参照 Bloom 分类法):
- 记忆(Remembering):复述
let、run、with、apply、also五个作用域函数的签名与返回值,识别其引用对象的两种方式(it与this)。 - 理解(Understanding):用二维分类法解释五个函数的差异(引用方式 × 返回值),阐述作用域函数在对象初始化、链式调用、空安全检查中的语义作用。
- 应用(Applying):在真实工程代码中正确选择作用域函数,避免误用导致的可读性下降与隐藏 bug,能够使用作用域函数构建简洁的链式调用与 DSL。
- 分析(Analyzing):剖析作用域函数的编译产物,理解
inline关键字对性能的影响,识别作用域函数在this隐式接收者冲突时的潜在歧义。 - 评估(Evaluating):评估作用域函数与显式变量赋值之间的可读性权衡,判断在团队编码规范中是否应当限制作用域函数的使用范围。
- 创造(Creating):设计自定义作用域函数,构建基于作用域函数的 DSL,编写团队级编码规范文档。
历史动机与背景
早期 Java 的冗余代码问题
在 Java 6/7 时代,开发者经常面对以下冗余代码:
// Java 风格:典型的"初始化-配置-使用"模式
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
String result = sb.toString();
// Java 风格:空检查后的处理
User user = getUser();
if (user != null) {
String name = user.getName();
System.out.println(name);
}
这种模式存在三个问题:
- 重复的变量名引用:
sb、user在每个语句中重复出现,增加视觉噪音。 - 临时变量污染作用域:
sb、user等中间变量在后续代码中可见,但实际只用一次。 - 空检查样板代码:
if (x != null) { ... }在 Java 中无处不在。
Groovy 与 Ruby 的启发
Groovy 在 2003 年左右引入了 with 方法,允许在闭包内用 it 引用对象:
def sb = new StringBuilder().with {
append("Hello")
append(", ")
append("World")
toString() // 返回值
}
Ruby 也提供了类似的 tap 方法(用于调试)和 then 方法(用于链式转换)。这些设计启发了 Kotlin 团队在语言层面提供更系统的作用域函数。
Kotlin 1.0 的标准化设计
Kotlin 1.0(2016 年 2 月发布)在标准库中一次性引入了五个作用域函数。设计团队(主要由 Andrey Breslav 主导)选择了不同的组合策略,覆盖两个维度的四种组合:
| 维度 | 维度取值 |
|---|---|
| 引用方式 | this(隐式接收者) / it(显式参数) |
| 返回值 | 对象本身 / Lambda 结果 |
五个函数覆盖了这四种组合中的一个或多个:
| 函数 | 引用方式 | 返回值 | 主要用途 |
|---|---|---|---|
let | it | Lambda 结果 | 空安全、转换 |
run | this | Lambda 结果 | 计算、初始化 |
with | this | Lambda 结果 | 配置对象(非扩展函数) |
apply | this | 对象本身 | 初始化、Builder 模式 |
also | it | 对象本身 | 副作用、链式调用 |
这种设计让开发者在不同场景下有明确的最优选择,而不是像 Groovy 那样只有一个 with 函数应对所有场景。
形式化定义
函数签名
五个作用域函数的标准签名如下:
// let:T 的扩展函数,参数为 (T) -> R,返回 R
public inline fun <T, R> T.let(block: (T) -> R): R
// run:T 的扩展函数,参数为 T.() -> R,返回 R
public inline fun <T, R> T.run(block: T.() -> R): R
// with:普通函数,参数为 T 和 T.() -> R,返回 R
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
// apply:T 的扩展函数,参数为 T.() -> Unit,返回 T
public inline fun <T> T.apply(block: T.() -> Unit): T
// also:T 的扩展函数,参数为 (T) -> Unit,返回 T
public inline fun <T> T.also(block: (T) -> Unit): T
类型论视角
从类型论角度,作用域函数都是**恒等函子(Identity Functor)**的变体。设 为恒等函子:
那么:
let实现了 map 操作:also实现了 tap(副作用):apply是also的this版本:
形式化地,apply 的语义可表示为:
而 let 的语义为:
内联语义
所有作用域函数都标记为 inline,意味着编译器会将函数调用内联到调用处。形式化地,对于 let:
这保证了作用域函数在运行时零开销——不会创建额外的 Lambda 对象(除非 Lambda 捕获了外部变量),也不会有函数调用开销。
理论推导
编译产物的对比
考虑以下代码:
val result = "hello".let { it.uppercase() }
由于 let 是 inline 函数,编译后的字节码等价于:
String result = "hello".toUpperCase();
类似地:
val sb = StringBuilder().apply {
append("a")
append("b")
}
编译后等价于:
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
这种内联特性使作用域函数在性能敏感场景下也完全可用。
Lambda 捕获的开销分析
虽然函数本身被内联,但 Lambda 表达式如果捕获了外部变量,仍会创建 Function1 对象。考虑:
val prefix = ">"
val result = "hello".let { prefix + it.uppercase() }
此时 Lambda 捕获了 prefix,编译后会生成类似以下的代码:
String prefix = ">";
String result = Functions1.invoke(prefix, "hello"); // 仍有对象分配
为了避免这种开销,Kotlin 编译器会尝试将捕获的局部变量作为参数传入内联后的代码块。但对于捕获的 var 变量或闭包变量,仍需包装为 Ref 对象。
this 与 it 的可读性权衡
this 与 it 的选择不仅是风格问题,还涉及作用域冲突:
- this 的优势:在配置对象时,
this.方法()可省略this.,代码简洁。 - this 的劣势:当 Lambda 内需要访问外层
this时,需用this@OuterClass限定,容易出错。 - it 的优势:显式参数,无歧义,可重命名(
let { value -> ... })。 - it 的劣势:每次调用都需写
it.方法(),配置代码较冗长。
返回值对链式调用的影响
返回 this 的 apply 和 also 适合链式调用:
val result = obj
.also { log("created") }
.apply { configure() }
.also { log("configured") }
返回 Lambda 结果的 let 和 run 适合链式转换:
val result = "hello"
.let { it.uppercase() }
.let { it + "!" }
.let { it.length }
两种返回值类型的组合形成了链式 DSL 的基础。
代码示例
示例 1:五个函数的基础用法
package fandex.scope.basic
/**
* 五大作用域函数基础用法对比。
* 通过同一个对象演示五个函数的行为差异。
*/
data class Person(var name: String, var age: Int) {
fun greet() = "Hi, I'm $name"
}
fun main() {
val person = Person("Alice", 30)
// let:用 it 引用,返回 Lambda 结果
val letResult: String = person.let {
it.name + " is " + it.age + " years old"
}
println("let: $letResult")
// run:用 this 引用,返回 Lambda 结果
val runResult: String = person.run {
"$name is $age years old" // 省略 this.
}
println("run: $runResult")
// with:非扩展函数,用 this 引用,返回 Lambda 结果
val withResult: String = with(person) {
greet() + " and I'm $age" // 省略 this.
}
println("with: $withResult")
// apply:用 this 引用,返回对象本身
val applyResult: Person = person.apply {
name = "Bob" // 等价于 this.name = "Bob"
age = 25
}
println("apply: $applyResult (person === applyResult: ${person === applyResult})")
// also:用 it 引用,返回对象本身
val alsoResult: Person = person.also {
it.name = "Charlie"
it.age = 40
}
println("also: $alsoResult (person === alsoResult: ${person === alsoResult})")
}
示例 2:空安全场景下的 let
package fandex.scope.nullsafety
/**
* let 在空安全检查中的应用。
* Kotlin 的 ?. 操作符与 let 组合,可以替代 Java 的 if-null 检查。
*/
data class User(val name: String, val email: String?)
fun main() {
val user: User? = getUserFromDb()
// 反模式:Java 风格的空检查
if (user != null) {
println("Name: ${user.name}")
println("Email: ${user.email ?: "N/A"}")
}
// Kotlin 风格:使用 ?.let
user?.let { u ->
println("Name: ${u.name}")
println("Email: ${u.email ?: "N/A"}")
}
// 注意:let 块内的 u 是非空类型,编译器智能转换
// 多个可空值的链式处理
val emailLength: Int? = user?.email?.let {
println("Processing email: $it")
it.length
}
println("Email length: $emailLength")
}
fun getUserFromDb(): User? = User("Alice", "alice@example.com")
示例 3:apply 用于对象初始化
package fandex.scope.apply
import java.util.Properties
import javax.sql.DataSource
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
/**
* apply 在对象初始化场景下的典型应用。
* 替代 Builder 模式,无需额外的 Builder 类。
*/
// 配置对象
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
var maxConnections: Int = 100
var timeoutMs: Long = 5000
var enableSsl: Boolean = false
override fun toString() = "ServerConfig(host=$host, port=$port, maxConn=$maxConnections)"
}
fun main() {
// 用 apply 初始化配置对象
val config = ServerConfig().apply {
host = "0.0.0.0"
port = 8443
maxConnections = 200
timeoutMs = 10000
enableSsl = true
}
println(config)
// 用 apply 配置 Java 标准库对象
val props = Properties().apply {
setProperty("user", "admin")
setProperty("password", "secret")
setProperty("url", "jdbc:postgresql://localhost/db")
}
println("Properties: ${props.getProperty("user")}")
// 用 apply 配置 HikariCP 数据源
val dataSource: DataSource = HikariDataSource().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
username = "dbuser"
password = "dbpass"
maximumPoolSize = 10
connectionTimeout = 30000
})
println("DataSource ready: ${dataSource.connection != null}")
}
示例 4:also 用于链式调用与副作用
package fandex.scope.also
/**
* also 在链式调用与副作用场景下的应用。
* also 的核心价值是"插入副作用而不打断链式调用"。
*/
data class HttpRequest(
val url: String,
val method: String,
val headers: Map<String, String>,
val body: String?
)
class HttpRequestBuilder {
var url: String = ""
var method: String = "GET"
private val headers = mutableMapOf<String, String>()
var body: String? = null
fun header(key: String, value: String) = apply {
headers[key] = value
}
fun build() = HttpRequest(url, method, headers.toMap(), body)
}
fun main() {
// 使用 also 在链式调用中插入日志
val request = HttpRequestBuilder()
.apply {
url = "https://api.example.com/users"
method = "POST"
body = """{"name": "Alice"}"""
}
.also { println("[构建] 请求对象已创建: $it") } // 副作用:日志
.also { validateRequest(it) } // 副作用:校验
.build()
.also { println("[完成] 最终请求: $it") }
println("\n最终请求 URL: ${request.url}")
}
fun validateRequest(builder: HttpRequestBuilder) {
require(builder.url.startsWith("http")) { "URL 必须以 http 开头" }
require(builder.method in listOf("GET", "POST", "PUT", "DELETE")) { "不支持的 HTTP 方法" }
}
示例 5:run 用于计算与初始化
package fandex.scope.run
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* run 在计算与一次性初始化场景下的应用。
* run 适合"创建一个临时作用域,计算并返回结果"。
*/
fun main() {
// 场景 1:计算某个复杂表达式的结果
val formattedDate = run {
val now = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
now.format(formatter)
}
println("Formatted date: $formattedDate")
// 场景 2:从文件读取配置
val config = run {
val file = File("config.properties")
if (file.exists()) {
file.readText()
} else {
"default config"
}
}
println("Config: $config")
// 场景 3:用 run 在对象上执行多个操作并返回结果
val file = File("output.txt")
val lineCount = file.run {
writeText("line1\nline2\nline3\n")
readLines().size
}
println("Line count: $lineCount")
// 场景 4:避免变量名污染
val result = run {
val tempData = loadTempData() // 临时变量
val processed = tempData.filter { it > 0 }
processed.sum()
} // tempData 与 processed 在此处作用域外不可见
println("Sum: $result")
}
fun loadTempData() = listOf(1, -2, 3, 4, -5)
示例 6:with 用于配置已有对象
package fandex.scope.with
import javax.swing.*
/**
* with 在配置已有对象场景下的应用。
* with 是唯一一个非扩展函数的作用域函数,适合"对某个对象进行一系列操作"。
*/
fun main() {
// 场景 1:配置 Swing 组件
val button = JButton()
with(button) {
text = "Click me"
toolTipText = "Click this button to submit"
bounds = java.awt.Rectangle(10, 10, 100, 30)
addActionListener { println("Button clicked!") }
}
println("Button: ${button.text}")
// 场景 2:对集合进行一系列操作
val numbers = mutableListOf(1, 2, 3, 4, 5)
with(numbers) {
add(6)
add(7)
removeAt(0)
shuffle()
}
println("Numbers: $numbers")
// 场景 3:构建字符串
val sb = StringBuilder()
with(sb) {
append("Name: Alice\n")
append("Age: 30\n")
append("Email: alice@example.com\n")
}
println("Profile:\n$sb")
}
示例 7:组合使用与 DSL
package fandex.scope.dsl
/**
* 作用域函数组合使用与 DSL 构建。
* 通过组合不同的作用域函数,可以构建出表达力强的 DSL。
*/
class HtmlBuilder {
private val elements = mutableListOf<String>()
fun head(block: HeadBuilder.() -> Unit) {
val builder = HeadBuilder()
builder.block()
elements.add("<head>${builder.build()}</head>")
}
fun body(block: BodyBuilder.() -> Unit) {
val builder = BodyBuilder()
builder.block()
elements.add("<body>${builder.build()}</body>")
}
fun build() = "<html>\n${elements.joinToString("\n")}\n</html>"
}
class HeadBuilder {
private val elements = mutableListOf<String>()
fun title(text: String) = elements.add("<title>$text</title>")
fun meta(name: String, content: String) = elements.add("""<meta name="$name" content="$content">""")
fun build() = elements.joinToString("\n")
}
class BodyBuilder {
private val elements = mutableListOf<String>()
fun h1(text: String) = elements.add("<h1>$text</h1>")
fun p(text: String) = elements.add("<p>$text</p>")
fun build() = elements.joinToString("\n")
}
fun html(block: HtmlBuilder.() -> Unit): String {
return HtmlBuilder().apply(block).build()
}
fun main() {
// 用作用域函数构建的 DSL
val document = html {
head {
title("My Page")
meta("description", "A demo page")
}
body {
h1("Welcome")
p("This is a paragraph.")
}
}
println(document)
// 组合 let、also、apply、run 处理复杂业务
val processed = listOf("Alice", "Bob", "Charlie")
.also { println("原始列表: $it") }
.map { it.uppercase() }
.also { println("大写转换: $it") }
.filter { it.length > 3 }
.let { it.joinToString(", ") }
.run { "Result: $this" }
println(processed)
}
对比分析
五个函数的二维分类表
| 函数 | 引用方式 | 返回值 | 是否扩展函数 | 典型用途 |
|---|---|---|---|---|
let | it | Lambda 结果 | 是 | 空安全、转换 |
run | this | Lambda 结果 | 是 | 计算、初始化 |
with | this | Lambda 结果 | 否(普通函数) | 配置已有对象 |
apply | this | 对象本身 | 是 | 初始化、Builder |
also | it | 对象本身 | 是 | 副作用、链式调用 |
let vs run:转换 vs 计算
| 维度 | let | run |
|---|---|---|
| 引用方式 | it(显式) | this(隐式) |
| 参数命名 | 可重命名(let { x -> ... }) | 不可命名 |
| 可读性 | 显式,无歧义 | 简洁,但有冲突风险 |
| 适用场景 | 空安全、转换、链式映射 | 计算、临时作用域 |
apply vs also:配置 vs 副作用
| 维度 | apply | also |
|---|---|---|
| 引用方式 | this | it |
| 典型场景 | 设置属性(name = "x") | 调用方法(log(it)) |
| 可读性 | 属性赋值简洁 | 方法调用清晰 |
| 风险 | this 冲突时易出错 | 无冲突 |
作用域函数 vs 显式变量赋值
| 维度 | 作用域函数 | 显式变量 |
|---|---|---|
| 代码行数 | 较少 | 较多 |
| 作用域 | 限制在 Lambda 内 | 持续到代码块结束 |
| 可读性 | 熟悉者高效,新手困惑 | 普遍清晰 |
| 调试 | Lambda 内断点需注意 | 直接断点 |
| 性能 | inline 零开销 | 零开销 |
与其他语言类似构造的对比
| 语言 | 类似构造 | 与 Kotlin 对应 |
|---|---|---|
| Java | 无原生支持 | 需用 Builder 模式 |
| Groovy | with { } | 类似 apply |
| Ruby | tap、then | also、let |
| Swift | withUnsafePointer 等 | 不直接对应 |
| Scala | 无内置,可用 implicit 类模拟 | 自定义扩展 |
| JavaScript | 无原生支持 | 需用 IIFE |
常见陷阱与反模式
陷阱 1:过度嵌套导致可读性下降
// 反模式:嵌套作用域函数导致代码难以阅读
val result = user?.let { u ->
u.address?.let { addr ->
addr.city?.let { city ->
city.zipCode?.let { zip ->
zip.prefix?.let { prefix ->
prefix.take(3)
}
}
}
}
}
// 正确做法:使用安全调用链
val result = user?.address?.city?.zipCode?.prefix?.take(3)
生产事故案例:某团队在重构用户档案模块时,过度使用 let 嵌套(4 层以上),导致后续维护时难以追踪逻辑。Code Review 时被指出改为安全调用链后,代码行数从 25 行减少到 1 行。
陷阱 2:apply 内部误用返回值
// 反模式:在 apply 内部尝试返回值
val config = Config().apply {
val computed = computeValue() // 这个 computed 只在 apply 内部可见
name = computed // 正确:赋值给属性
// return computed // 编译错误
computed // 无效:apply 返回的是 this,不是 computed
}
// 正确做法:用 run 计算后赋值
val config = Config().apply {
name = computeValue()
}
陷阱 3:this 引用歧义
class Outer {
val name = "Outer"
fun test() {
val person = Person("Alice", 30)
// 反模式:在 apply 内部,this 指向 person,导致外层 this 被遮蔽
person.apply {
println(this.name) // 输出 "Alice",而非 "Outer"
println(this@Outer.name) // 输出 "Outer",需显式限定
}
// 正确做法:用 also 避免歧义
person.also {
println(this.name) // 输出 "Outer",this 指向 Outer
println(it.name) // 输出 "Alice"
}
}
}
data class Person(val name: String, val age: Int)
生产事故案例:某 Android 应用在 Activity 内使用 apply 配置 View,误将 this 当作 Activity,导致调用 findViewById 等 Activity 方法时报 NoSuchMethodError。修复方案:在 apply 内部访问外层对象时使用 this@Activity 限定。
陷阱 4:在 also 中修改对象状态
// 反模式:also 语义上应该是"副作用"(不修改对象),但实际可以修改
val list = mutableListOf(1, 2, 3)
list.also {
it.add(4) // 修改了对象,违背 also 的语义
}
// 正确做法:用 apply 修改对象状态
list.apply {
add(4) // 语义清晰:修改对象
}
// 或者:用 also 做纯副作用
list.also {
println("Before modification: $it") // 只读,不修改
}
虽然 Kotlin 编译器不强制约束,但社区共识是:
apply:修改对象状态also:仅做副作用(日志、校验、通知),不修改对象
陷阱 5:let 与 ?: 的优先级混淆
val user: User? = getUser()
// 反模式:let 与 ?: 优先级混淆
val result = user?.let { it.name } ?: "Anonymous"
// 期望:user 不为空时返回 name,否则返回 "Anonymous"
// 实际:正确(let 优先级高于 ?:)
// 但这种写法更清晰:
val result = if (user != null) user.name else "Anonymous"
// 或
val result = user?.name ?: "Anonymous"
陷阱 6:在循环中误用 apply 创建多个对象
// 反模式:在循环中误用 apply,每次都创建新对象
val configs = mutableListOf<Config>()
for (i in 1..10) {
configs.add(Config().apply {
id = i
name = "config_$i"
})
}
// 正确做法:用 map 一气呵成
val configs = (1..10).map { i ->
Config().apply {
id = i
name = "config_$i"
}
}
工程实践
实践 1:团队编码规范建议
package fandex.scope.guidelines
/**
* 团队级作用域函数使用规范(建议)。
*
* 规范要点:
* 1. 优先使用最语义化的函数
* 2. 避免超过 2 层嵌套
* 3. Lambda 内代码不超过 10 行
* 4. 配置对象用 apply,副作用用 also
* 5. 空安全用 ?.let,但避免过度嵌套
*/
// 推荐:用 apply 配置对象
val server = Server().apply {
host = "0.0.0.0"
port = 8080
maxConnections = 100
}
// 推荐:用 also 插入日志或校验
server.also {
log("Server created: $it")
validate(it)
}
// 推荐:用 let 做链式转换
val result = getUser()
?.let { it.name }
?.let { it.uppercase() }
?.let { "Hello, $it" }
// 不推荐:超过 2 层嵌套
// val x = a?.let { b?.let { c?.let { ... } } }
// 不推荐:Lambda 内代码过长
// val x = obj.apply {
// // 20 行配置代码...应该提取为独立函数
// }
// 推荐:长配置提取为独立函数
val server = Server().apply {
configureDefaults()
configureSsl()
}
fun Server.configureDefaults() {
host = "0.0.0.0"
port = 8080
}
fun Server.configureSsl() {
enableSsl = true
certPath = "/etc/ssl/cert.pem"
}
class Server {
var host: String = ""
var port: Int = 0
var maxConnections: Int = 0
var enableSsl: Boolean = false
var certPath: String = ""
}
fun log(msg: String) = println(msg)
fun validate(s: Server) = require(s.port in 1..65535)
fun getUser() = User("Alice")
data class User(val name: String)
实践 2:性能敏感场景的使用
package fandex.scope.performance
/**
* 性能敏感场景下的作用域函数使用建议。
*
* 核心原则:
* 1. 作用域函数本身零开销(inline)
* 2. 但 Lambda 捕获外部变量仍会创建 Function 对象
* 3. 热路径下避免捕获 var 变量
*/
// 反模式:热路径中捕获 var 变量
fun inefficient(data: List<Int>): Int {
var sum = 0
data.forEach {
sum += it // 捕获 sum(Ref 对象)
}
return sum
}
// 推荐:避免捕获,直接使用 for 循环
fun efficient(data: List<Int>): Int {
var sum = 0
for (item in data) {
sum += item
}
return sum
}
// 推荐:用 fold/reduce 避免捕获
fun functional(data: List<Int>): Int {
return data.fold(0) { acc, item -> acc + item }
}
// 在初始化阶段,作用域函数无性能差异
class Config {
var a: Int = 0
var b: Int = 0
var c: Int = 0
}
fun main() {
// 这两种写法在编译后性能完全一致
val config1 = Config().apply {
a = 1
b = 2
c = 3
}
val config2 = Config().also {
it.a = 1
it.b = 2
it.c = 3
}
val config3 = Config().also {
val c = it // 局部变量,避免重复解引用
c.a = 1
c.b = 2
c.c = 3
}
}
实践 3:测试中的应用
package fandex.scope.testing
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
/**
* 作用域函数在测试代码中的应用。
* 测试代码常需要构建复杂对象,作用域函数能显著简化。
*/
data class TestUser(
val id: String,
val name: String,
val email: String?,
val roles: List<String>
)
class UserService {
fun findById(id: String): TestUser? =
if (id == "1") TestUser("1", "Alice", "alice@example.com", listOf("admin"))
else null
fun findAll(): List<TestUser> = listOf(
TestUser("1", "Alice", "alice@example.com", listOf("admin")),
TestUser("2", "Bob", null, listOf("user"))
)
}
class UserServiceTest {
private val service = UserService()
@Test
fun `find by id returns user when exists`() {
// 用 let 简化空安全断言
val user = service.findById("1")
user?.let {
assertEquals("Alice", it.name)
assertNotNull(it.email)
assertEquals(listOf("admin"), it.roles)
} ?: error("User should not be null")
}
@Test
fun `find by id returns null when not exists`() {
val user = service.findById("999")
assertEquals(null, user)
}
@Test
fun `find all returns all users`() {
val users = service.findAll()
// 用 also 打印调试信息
users.also { println("Found ${it.size} users") }
assertEquals(2, users.size)
assertEquals("Alice", users[0].name)
}
@Test
fun `build test fixture with apply`() {
// 用 apply 构建测试数据
val expected = TestUser(
id = "1",
name = "Alice",
email = "alice@example.com",
roles = listOf("admin")
)
val actual = service.findById("1")
assertEquals(expected, actual)
}
}
实践 4:Android View 绑定
package fandex.scope.android
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
/**
* Android 开发中作用域函数的典型应用:View 绑定与 RecyclerView Adapter。
*/
class UserAdapter(
private val users: List<String>
) : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
class ViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// 用 apply 配置 TextView
val textView = TextView(parent.context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
textSize = 16f
setPadding(16, 16, 16, 16)
}
return ViewHolder(textView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 用 also 添加日志
holder.textView.text = users[position].also {
println("Binding user: $it")
}
}
override fun getItemCount() = users.size
}
// 在 Activity 中使用
class MainActivity {
private var adapter: UserAdapter? = null
fun onCreate() {
// 用 ?.let 做空安全
adapter?.let {
it.updateData(listOf("Alice", "Bob"))
}
}
}
// 模拟类
class UserAdapter2(private val users: List<String>) {
fun updateData(newUsers: List<String>) {}
}
案例研究
案例 1:某金融系统配置加载模块
业务场景:某金融系统需要从多个数据源(数据库、配置文件、环境变量)加载配置,并合并为统一的配置对象。
重构前(Java 风格):
fun loadConfig(): SystemConfig {
val config = SystemConfig()
val dbConfig = loadFromDb()
config.dbHost = dbConfig.host
config.dbPort = dbConfig.port
val fileConfig = loadFromFile()
config.fileTimeout = fileConfig.timeout
config.fileRetry = fileConfig.retry
val envConfig = loadFromEnv()
config.envName = envConfig.name
return config
}
重构后(使用作用域函数):
fun loadConfig(): SystemConfig = SystemConfig().apply {
loadFromDb().let {
dbHost = it.host
dbPort = it.port
}
loadFromFile().let {
fileTimeout = it.timeout
fileRetry = it.retry
}
loadFromEnv().also {
println("Loading from env: ${it.name}") // 副作用
}.let {
envName = it.name
}
}
效果:代码行数从 12 行减少到 10 行,更重要的是消除了中间变量,配置逻辑一目了然。
案例 2:某电商 App 购物车计算
业务场景:购物车需要计算总价、应用折扣、添加运费,并返回最终价格。
实现:
data class CartItem(val name: String, val price: Double, val quantity: Int)
data class Discount(val code: String, val percentage: Double)
class ShoppingCart {
private val items = mutableListOf<CartItem>()
private var discount: Discount? = null
fun addItem(item: CartItem) = apply { items.add(item) }
fun applyDiscount(code: String, percentage: Double) = apply {
discount = Discount(code, percentage)
}
fun calculateTotal(): Double = items
.map { it.price * it.quantity }
.sum()
.let { total ->
discount?.let { total * (1 - it.percentage / 100) } ?: total
}
.let { it + 10.0 } // 运费
.also { println("Final total: $it") }
}
fun main() {
val cart = ShoppingCart()
.apply {
addItem(CartItem("Book", 20.0, 2))
addItem(CartItem("Pen", 5.0, 3))
applyDiscount("SAVE10", 10.0)
}
.also { println("Cart created with ${it.itemCount} items") }
val total = cart.calculateTotal()
println("Total: $total")
}
案例 3:某后端服务 HTTP 客户端构建
业务场景:某微服务需要构建一个高度可配置的 HTTP 客户端,支持超时、重试、拦截器等。
实现:
import java.net.http.HttpClient
import java.time.Duration
class HttpClientBuilder {
private var connectTimeout: Duration = Duration.ofSeconds(30)
private var requestTimeout: Duration = Duration.ofSeconds(60)
private val interceptors = mutableListOf<(String) -> String>()
private var retryCount: Int = 0
fun connectTimeout(duration: Duration) = apply { connectTimeout = duration }
fun requestTimeout(duration: Duration) = apply { requestTimeout = duration }
fun addInterceptor(interceptor: (String) -> String) = apply { interceptors.add(interceptor) }
fun retry(count: Int) = apply { retryCount = count }
fun build(): HttpClient = HttpClient.newBuilder()
.connectTimeout(connectTimeout)
.also {
println("Building client with timeout: $connectTimeout")
}
.also {
if (retryCount > 0) {
println("Retry enabled: $retryCount times")
}
}
.build()
}
fun main() {
val client = HttpClientBuilder()
.connectTimeout(Duration.ofSeconds(10))
.requestTimeout(Duration.ofSeconds(30))
.addInterceptor { url -> "Bearer token" }
.retry(3)
.also { println("Builder configured") }
.build()
.also { println("Client built: $it") }
}
习题
基础题
习题 1:以下代码的输出是什么?
val result = "hello".let {
it.uppercase()
}.run {
this + "!"
}.also {
println(it)
}
println("Final: $result")
参考答案要点:
- 输出顺序:
HELLO!(also 内的 println)Final: HELLO!
- 关键点:
let返回 Lambda 结果"HELLO"run返回 Lambda 结果"HELLO!"also返回对象本身"HELLO!",同时打印it
习题 2:用 apply 简化以下代码。
val list = mutableListOf<Int>()
list.add(1)
list.add(2)
list.add(3)
参考答案要点:
val list = mutableListOf<Int>().apply {
add(1)
add(2)
add(3)
}
进阶题
习题 3:分析以下代码的问题并给出改进方案。
val user = getUser()
val name = user?.let { it?.name }
参考答案要点:
- 问题:
it已经是非空类型(?.let已保证),it?.name中的?.多余 - 改进:
val name = user?.let { it.name }
// 或更简洁:
val name = user?.name
习题 4:实现一个自定义作用域函数 tap,行为类似 Ruby 的 tap:接收一个对象,执行副作用函数,返回原对象。
参考答案要点:
inline fun <T> T.tap(block: (T) -> Unit): T {
block(this)
return this
}
// 使用
val result = listOf(1, 2, 3)
.tap { println("Original: $it") }
.map { it * 2 }
.tap { println("After map: $it") }
.filter { it > 2 }
.tap { println("After filter: $it") }
挑战题
习题 5:在不使用 apply 的情况下,用 also 模拟 apply 的行为,并分析二者的差异。
参考答案要点:
inline fun <T> T.applyImitation(block: T.() -> Unit): T = also {
it.block() // 在 also 内部调用 block,将 this 设为 it
}
// 差异分析:
// 1. apply 直接以 this 调用 block,编译器内联后无任何额外开销
// 2. applyImitation 在 also 内部调用 block,多了一层函数调用
// 3. applyImitation 中的 block 是 T.() -> Unit,调用时需通过 it 访问
// 4. 真正的 apply 中 block 是 T.() -> Unit,调用时 this 直接是接收者
习题 6:分析作用域函数在 Kotlin DSL 中的核心作用,并以 kotlinx.html 库为例说明。
参考答案要点:
- 核心作用:
- 提供隐式接收者,简化嵌套 DSL
- 控制作用域,限制子 DSL 只能访问特定方法
- 通过
inline保证零开销
kotlinx.html示例:
html {
head {
title { +"My Page" }
}
body {
div {
p { +"Hello" }
}
}
}
- 每个块都是一个
Tag.() -> Unit类型的 Lambda,this是对应的 Tag 对象 - 子 DSL 只能调用该 Tag 的方法,编译器保证类型安全
参考文献
[1] Breslav, A. 2016. Kotlin 1.0 Released: Pragmatic Language for JVM and Android. JetBrains Blog. https://blog.jetbrains.com/kotlin/2016/02/1-0-released/
[2] Jemerov, D. and Isakova, S. 2017. Kotlin in Action. Manning Publications. ISBN: 978-1617293280
[3] Kotlin Standard Library Documentation. 2024. Scope Functions. https://kotlinlang.org/docs/scope-functions.html
[4] Elizarov, R. 2017. Inline Functions in Kotlin. Kotlin Blog. https://elizarov.medium.com/inline-functions-in-kotlin-5e8b0a1e8c6e
[5] Bloch, J. 2018. Effective Java (3rd Edition). Addison-Wesley Professional. ISBN: 978-0134685991
[6] Skeet, J. 2019. C# in Depth (4th Edition). Manning Publications. ISBN: 978-1617294532
[7] Ierusalimschy, R., de Figueiredo, L. H., and Celes, W. 2018. Passing a language through the eye of a needle. Communications of the ACM 61, 9 (Sep. 2018), 38-45. https://doi.org/10.1145/3230624
[8] Ruby Documentation. 2024. Object#tap. https://ruby-doc.org/core/Object.html#method-i-tap
[9] Groovy Documentation. 2024. with() method. https://docs.groovy-lang.org/latest/html/groovy-jdk/java/lang/Object.html#with(groovy.lang.Closure)
[10] TypeScript Handbook. 2024. Utility Types. https://www.typescriptlang.org/docs/handbook/utility-types.html
延伸阅读
官方文档
- Kotlin Scope Functions:https://kotlinlang.org/docs/scope-functions.html
- 官方对五个作用域函数的详细说明与选型建议
- Kotlin Inline Functions:https://kotlinlang.org/docs/inline-functions.html
- 理解
inline关键字对作用域函数性能的影响
- 理解
- Kotlin Null Safety:https://kotlinlang.org/docs/null-safety.html
?.let模式的理论基础
经典教材
- 《Kotlin in Action》(Dmitry Jemerov、Svetlana Isakova 著):第 5 章对作用域函数有详细讨论
- 《Effective Kotlin》(Marcin Moskala 著):包含多个关于作用域函数使用的最佳实践条目
- 《Kotlin Cookbook》(Ken Kousen 著):实战场景下的作用域函数应用
前沿论文
- Designing DSLs in Kotlin(Roman Elizarov, 2018):作用域函数在 DSL 设计中的核心作用
- Inline Functions for Performance(Kotlin Team, 2017):inline 关键字的实现原理
开源项目
- kotlinx.html:https://github.com/Kotlin/kotlinx.html
- 大量使用作用域函数构建 HTML DSL
- Ktor:https://github.com/ktorio/ktor
- 路由配置 DSL 使用作用域函数
- Gradle Kotlin DSL:https://github.com/gradle/kotlin-dsl
- 构建脚本的 DSL 基于 Kotlin 作用域函数
社区资源
- Kotlin Slack:#stdlib 频道讨论作用域函数
- Marcin Moskala 博客:https://marcinmoskala.com/
- 多篇关于作用域函数选型的深度文章
- Kotlin Weekly:定期推送作用域函数相关文章