Kotlin属性委托

34 阅读4分钟

属性委托是 Kotlin 中一项强大的特性,它允许你将一个属性的 getter 和 setter 的逻辑委托给一个独立的类来处理。这可以让你重用代码、分离关注点,并让你的类更简洁。

1. 核心概念

委托模式的核心思想是:一个对象(委托方)将其部分职责交给另一个对象(被委托方)来完成

在 Kotlin 中,对于属性来说,这意味着:

  • 当你访问(get)一个属性时,Kotlin 会自动调用委托对象的 getValue 方法。
  • 当你修改(set)一个 var 属性时,Kotlin 会自动调用委托对象的 setValue 方法。

你只需要使用 by 关键字来指定委托对象即可。

2. 语法

class MyClass {
    var/val propertyName: PropertyType by Delegate()
}
  • propertyName: 你的属性名称。
  • PropertyType: 属性的类型。
  • by: 关键字,指定后面的对象是这个属性的委托。
  • Delegate(): 委托对象。这个类必须提供 getValue 方法,对于 var 属性,还必须提供 setValue 方法。

3. 委托的实现要求

为了成为一个属性的委托,一个类必须满足以下条件:

对于 val 属性(只读):

  • 必须有一个 operator fun getValue(thisRef: Any?, property: KProperty<*>): T 函数。

    • thisRef: 指向拥有该属性的对象(即委托方)。
    • property: 指向该属性的元数据,例如属性名。
    • 返回值 T: 必须与属性的类型一致。

对于 var 属性(可读写):

  • 除了 getValue,还必须有一个 operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) 函数。

    • value: 要赋给属性的值,类型必须与属性类型一致。

4. 自定义委托示例

让我们创建一个简单的委托,它会在属性被访问或修改时打印日志。

import kotlin.reflect.KProperty

// 1. 定义一个委托类
class LoggingDelegate<T> {
    private var storedValue: T? = null

    // 2. 实现 getValue 方法
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("${property.name} 被访问,当前值为: $storedValue")
        return storedValue as T
    }

    // 3. 实现 setValue 方法 (仅用于 var 属性)
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("${property.name} 被修改,新值为: $value")
        storedValue = value
    }
}

// 4. 使用委托
class Person {
    var name: String by LoggingDelegate()
    var age: Int by LoggingDelegate()
}

fun main() {
    val person = Person()

    person.name = "Alice" // 输出: name 被修改,新值为: Alice
    person.age = 30      // 输出: age 被修改,新值为: 30

    println("Name is: ${person.name}") // 输出: name 被访问,当前值为: Alice
                                       //      Name is: Alice
    println("Age is: ${person.age}")   // 输出: age 被访问,当前值为: 30
                                       //      Age is: 30
}

这个例子清晰地展示了委托如何拦截属性的访问和修改。Person 类本身非常干净,它将日志记录的职责完全交给了 LoggingDelegate

5. 标准库中的委托

Kotlin 标准库提供了一些非常实用的委托,你可以直接使用它们来解决常见问题。

a. lazy - 延迟初始化

lazy 允许你将一个属性的初始化推迟到它第一次被访问的时候。这对于初始化成本较高的对象非常有用。

val expensiveObject: ExpensiveClass by lazy {
    println("Initializing expensive object...")
    ExpensiveClass() // 这个代码块只会执行一次
}

class ExpensiveClass {
    init {
        println("ExpensiveClass instance created.")
    }
}

fun main() {
    println("Program started.")
    
    // 此时 expensiveObject 还未被初始化
    val obj1 = expensiveObject // 第一次访问,触发初始化
    val obj2 = expensiveObject // 后续访问,直接返回已创建的实例

    println("obj1 === obj2: ${obj1 === obj2}") // 输出: true
}

输出:

plaintext

Program started.
Initializing expensive object...
ExpensiveClass instance created.
obj1 === obj2: true

b. observable - 可观察属性

Delegates.observable 可以让你监控一个属性的变化。当属性的值被修改时,它会触发一个回调函数。

import kotlin.properties.Delegates

class Person {
    var name: String by Delegates.observable("<no name>") { 
        property, oldValue, newValue ->
        println("${property.name} changed from $oldValue to $newValue")
    }
}

fun main() {
    val person = Person()
    person.name = "Alice" // 输出: name changed from <no name> to Alice
    person.name = "Bob"   // 输出: name changed from Alice to Bob
}

c. vetoable - 可否决属性

Delegates.vetoable 与 observable 类似,但它的回调函数可以返回一个 Boolean 值来决定是否允许这次修改。

import kotlin.properties.Delegates

class Person {
    var age: Int by Delegates.vetoable(0) { 
        property, oldValue, newValue ->
        println("Trying to change ${property.name} from $oldValue to $newValue")
        newValue >= 0 // 只有当新值大于等于0时,修改才被允许
    }
}

fun main() {
    val person = Person()
    
    person.age = 25 // 输出: Trying to change age from 0 to 25
    println(person.age) // 输出: 25

    person.age = -5  // 输出: Trying to change age from 25 to -5
    println(person.age) // 输出: 25 (修改被否决,值保持不变)
}

6. 委托给另一个属性

你也可以将一个属性的委托目标设置为同一个类或另一个对象的另一个属性

class Person {
    private var _name: String = "Default"

    var name: String
        get() = _name
        set(value) { _name = value }

    // 将 fullName 的 get 和 set 委托给 name
    var fullName: String by this::name
}

fun main() {
    val person = Person()
    
    person.fullName = "Alice Smith"
    println(person.name)      // 输出: Alice Smith
    println(person.fullName)  // 输出: Alice Smith

    person.name = "Bob Brown"
    println(person.fullName)  // 输出: Bob Brown
}

这在处理兼容性或需要别名属性时非常方便。

总结

属性委托是 Kotlin 中一个非常优雅和强大的特性,它可以帮助你:

  • 减少样板代码:将通用逻辑(如日志、验证、通知)封装到委托类中。
  • 实现关注点分离:让你的业务类只关注其核心职责。
  • 轻松实现设计模式:如观察者模式(通过 observable)、单例模式(通过 lazy)等。
  • 扩展现有类:在不修改类源代码的情况下,为其属性添加额外行为。

标准库中的 lazyobservable, 和 vetoable 是日常开发中非常常用的工具,而自定义委托则为你提供了无限的可能性来解决特定领域的问题。