2-2-23 快速掌握Kotlin-apply函数详解

14 阅读4分钟

Kotlin apply 函数详解

apply 是 Kotlin 标准库中的一个作用域函数(scope function),用于在对象上执行一个代码块,并返回该对象本身。

基本概念

函数签名

inline fun <T> T.apply(block: T.() -> Unit): T

核心特性

  1. 接收者对象:在 lambda 内部,this 指向调用 apply 的对象
  2. 返回值:返回对象本身(返回 this
  3. 典型用途:对象配置和初始化

基本使用

1. 对象初始化

class Person {
    var name: String = ""
    var age: Int = 0
    var city: String = ""
    
    override fun toString(): String = "Person(name='$name', age=$age, city='$city')"
}

fun main() {
    val person = Person().apply {
        name = "Alice"  // this.name = "Alice"
        age = 25
        city = "New York"
    }
    
    println(person)  // Person(name='Alice', age=25, city='New York')
}

2. 与构造器配合使用

class User(val id: String) {
    var name: String = ""
    var email: String = ""
    
    fun display() = "User(id=$id, name=$name, email=$email)"
}

fun main() {
    val user = User("123").apply {
        name = "Bob"
        email = "bob@example.com"
    }
    
    println(user.display())
}

apply 与普通初始化对比

不使用 apply

val person = Person()
person.name = "Alice"
person.age = 25
person.city = "New York"

使用 apply

val person = Person().apply {
    name = "Alice"
    age = 25
    city = "New York"
}

优势

  • 代码更紧凑
  • 避免重复书写对象变量名
  • 形成逻辑上的代码块

实际应用场景

1. Android 开发中的视图配置

val textView = TextView(context).apply {
    text = "Hello, Kotlin!"
    textSize = 16f
    setTextColor(Color.BLACK)
    gravity = Gravity.CENTER
    setPadding(16, 16, 16, 16)
}

2. 集合和容器的初始化

// 初始化列表
val numbers = mutableListOf<Int>().apply {
    for (i in 1..10) add(i * i)
    addAll(listOf(100, 121, 144))
    sortDescending()
}

// 初始化 Map
val config = mutableMapOf<String, Any>().apply {
    put("host", "localhost")
    put("port", 8080)
    put("timeout", 30)
    put("retry", true)
}

3. Builder 模式替代方案

data class Car(
    val brand: String,
    val model: String,
    val year: Int,
    var color: String = "White",
    var price: Double = 0.0
)

val myCar = Car("Tesla", "Model 3", 2023).apply {
    color = "Red"
    price = 45000.0
}

4. 复杂对象的链式配置

class DatabaseConfig {
    var host: String = "localhost"
    var port: Int = 3306
    var username: String = "root"
    var password: String = ""
    var database: String = "test"
    
    override fun toString(): String {
        return "DatabaseConfig(host='$host', port=$port, username='$username', database='$database')"
    }
}

fun main() {
    val config = DatabaseConfig().apply {
        host = "192.168.1.100"
        port = 5432
        username = "admin"
        password = "secret123"
        database = "production"
    }
    
    println(config)
}

apply 与其他作用域函数对比

函数接收者引用返回值典型用途
applythis上下文对象对象配置
letitLambda 结果空检查、转换
runthisLambda 结果对象计算、链式调用
withthisLambda 结果非扩展函数,在对象上操作
alsoit上下文对象附加操作、日志记录

对比示例

class User(var name: String, var age: Int)

fun main() {
    val user = User("Alice", 25)
    
    // apply - 返回对象本身
    val applyResult = user.apply {
        name = "Bob"
        age = 30
    }  // 返回 user 对象
    
    // let - 返回 lambda 结果
    val letResult = user.let {
        it.name = "Charlie"
        it.age = 35
        "Name changed"  // 返回字符串
    }  // 返回 "Name changed"
    
    // also - 返回对象本身
    val alsoResult = user.also {
        it.name = "David"
        it.age = 40
    }  // 返回 user 对象
    
    // run - 返回 lambda 结果
    val runResult = user.run {
        name = "Eve"
        age = 45
        "User updated"  // 返回字符串
    }  // 返回 "User updated"
}

链式调用

1. 多级配置

data class Address(var street: String = "", var city: String = "")
data class Company(var name: String = "", var address: Address = Address())

val company = Company().apply {
    name = "Tech Corp"
    address.apply {
        street = "123 Main St"
        city = "San Francisco"
    }
}

2. 复杂对象构建

class Order {
    var id: String = ""
    var items = mutableListOf<String>()
    var total: Double = 0.0
    
    fun addItem(item: String, price: Double) {
        items.add(item)
        total += price
    }
}

val order = Order().apply {
    id = "ORD-001"
    addItem("Laptop", 999.99)
    addItem("Mouse", 29.99)
    addItem("Keyboard", 89.99)
}

与空安全结合

1. 安全调用

class Config {
    var value: String? = null
}

fun processConfig(config: Config?) {
    config?.apply {
        value = "initialized"
        // 这里可以安全地访问 config 的属性和方法
        println("Config processed: $value")
    }
}

2. 替代 if 非空检查

fun updateUser(user: User?) {
    // 传统方式
    if (user != null) {
        user.name = "Updated"
        user.age = 30
    }
    
    // 使用 apply 的方式
    user?.apply {
        name = "Updated"
        age = 30
    }
}

性能注意事项

1. apply 是内联函数

apply 函数使用了 inline 修饰符,这意味着在编译时,lambda 代码会被内联到调用处,不会产生额外的函数对象开销。

// 源代码
val result = obj.apply { doSomething() }

// 编译后(大致等效)
val result = obj
obj.doSomething()

2. 与 also 的选择

  • 使用 apply 当需要在 lambda 内访问多个成员时(通过 this
  • 使用 also 当参数名 it 能提供更好可读性时
// apply - 适合配置多个属性
val user = User().apply {
    name = "Alice"
    age = 25
    email = "alice@example.com"
}

// also - 适合单个操作或日志记录
val user = User().also {
    it.name = "Alice"
    println("User created: ${it.name}")
}

高级用法

1. DSL(领域特定语言)构建

class HTML {
    private val children = mutableListOf<String>()
    
    fun div(block: Div.() -> Unit) {
        val div = Div().apply(block)
        children.add(div.toString())
    }
    
    override fun toString() = children.joinToString("\n")
}

class Div {
    var id: String = ""
    var className: String = ""
    
    fun p(text: String) = "<p>$text</p>"
    
    override fun toString() = 
        "<div id='$id' class='$className'>${/* 子元素 */}</div>"
}

fun html(block: HTML.() -> Unit): HTML {
    return HTML().apply(block)
}

fun main() {
    val page = html {
        div {
            id = "header"
            className = "main-header"
        }
        div {
            id = "content"
            className = "main-content"
        }
    }
    
    println(page)
}

2. 配置构建器模式

class ConfigurationBuilder {
    var host: String = "localhost"
    var port: Int = 8080
    var timeout: Int = 30
    var retries: Int = 3
    
    fun build(): Configuration = Configuration(host, port, timeout, retries)
}

data class Configuration(val host: String, val port: Int, val timeout: Int, val retries: Int)

fun createConfig(block: ConfigurationBuilder.() -> Unit): Configuration {
    return ConfigurationBuilder().apply(block).build()
}

fun main() {
    val config = createConfig {
        host = "api.example.com"
        port = 443
        timeout = 60
        retries = 5
    }
    
    println(config)
}

常见陷阱和最佳实践

1. 不要过度嵌套

// 避免 - 过度嵌套降低可读性
val result = SomeClass().apply {
    property1.apply {
        subProperty.apply {
            // 太深了!
        }
    }
}

// 建议 - 适度使用
val result = SomeClass().apply {
    property1 = configureProperty1()
    property2 = configureProperty2()
}

fun configureProperty1(): PropertyType {
    return PropertyType().apply {
        // 配置逻辑
    }
}

2. 明确返回意图

// 清晰 - 明确表示这是初始化
val person = Person().apply {
    name = "Alice"
    age = 25
}

// 混淆 - 不要这样使用,会让人疑惑返回值是什么
fun createPerson(): Person {
    return Person().apply {
        name = "Alice"
        age = 25
        // 如果这里添加了 return,会改变返回值!
    }
}

3. 与构造函数参数的区分

data class Employee(
    val id: String,
    val name: String
) {
    var department: String = ""
    var salary: Double = 0.0
}

// 使用 apply 配置可选属性
val employee = Employee("E001", "John Doe").apply {
    department = "Engineering"
    salary = 75000.0
}

总结

apply 函数是 Kotlin 中非常实用的工具,主要优势包括:

  1. 简化对象初始化:将多个属性设置集中在一个代码块中
  2. 提高代码可读性:形成逻辑上的初始化单元
  3. 支持链式调用:便于构建复杂对象
  4. 类型安全:在 lambda 内部可以直接访问对象的成员
  5. 性能良好:作为内联函数没有运行时开销

使用时机:

  • 当需要配置对象多个属性时
  • 构建复杂对象或 DSL 时
  • 替代传统的 Builder 模式
  • 在 Android 中配置视图组件时

记住:apply 返回的是对象本身,这使得它非常适合用于初始化配置场景,但不适合需要返回其他值的场景。