[kotlin]-关键字 by 委托

348 阅读2分钟

委托

介绍

类委托允许一个类将自身的部分实现委托给另一个类完成。
委托模式提高了代码复用性,通过委托可将通用逻辑提取到其他类中, keeping 使原类职责更加单一清晰。并且可以非侵入式地扩展类的功能。

优点
  1. 可以消除冗余代码: 委托模式让两个类建立关联,可以提取公共代码到委托类中,消除冗余。
  2. 提高代码复用: 委托类封装通用功能和行为,可以被多个其他类重复使用。
  3. 增强扩展性: 可以通过委托类继承和组合来扩展功能,而无需修改原有类。
  4. 实现声明式设计: 委托后的类接口更加简洁,只关注自身逻辑。
  5. 符合单一职责原则: 委托类只负责被抽象的共性功能,原类保留业务聚焦。
  6. 统一维护: 可以在委托类中集中维护公共代码,一处修改多处生效。

类委托

在买火车票的时候,经常会通过第三方平台进行购买,我们提供钱,他们帮我们购票抢票,人物本身不需要知道里面的具体逻辑

interface TicketPlatform {
    fun printTicket(money: Int) //出票
}
open class XcTicket : TicketPlatform {
    override fun printTicket(money: Int) {
        println("XcTicket bought you a ticket for $money yuan  ~")
    }
}

class User(ticketPlatform: TicketPlatform) : TicketPlatform by ticketPlatform {
}

//测试
val xcTicket = XcTicket()
//将User的购票逻辑 ,委托给协程购票,本身不关注该逻辑业务,保证User类职责更加清晰
User(xcTicket).printTicket(10) //XcTicket bought you a ticket for 10 yuan  ~

总结: 上面例子基本满足了委托的所有优点,User无需通过手动TicketPlatform的实例,也可以直接调用TicketPlatform的方法(类似于继承),众所周知kotlin 和 java 一样,都只能单继承,但是通过委托,User 可以实现多委托效果,从而达到多继承效果

属性委托

by 不仅可以让类委托,也可以让属性进行委托,属性委托更常见,它可以将属性的访问器逻辑委托给其他类处理,语法格式

    val/var <属性名>: <类型> by <表达式>
    val/var name: String by Delegate()

官方常见三种属性委托:

  • 延迟属性(lazy properties): 其值只在首次访问时计算。
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知。
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
属性委托的原理

在 by 后面的表达式是该 委托的实现, 因为属性对应的 get()(与 set())会被委托给它的 getValue() 与 setValue() 方法。 属性的委托不必实现接口,但是需要提供一个 getValue() 函数(对于 var 属性还有 setValue()

//  委托的属性
var workName :String by WorkNameDelegate()

//  被委托属性的Set/Get实现
class WorkNameDelegate {

    private var value : String = "Worker"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef ,他的 默认 ${property.name} 是 $value !"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value
        println("$thisRef ,他的 ${property.name} 被设置为 $value !")
    }

}
//测试
fun main() {
    val worker = Worker()
    println( worker.workName)
    worker.workName = "Android Developer Manager"
    println( worker.workName)
}
//输出
com.gx.kt.study.by.cls.Worker@50f8360d ,他的 默认 workName 是 Worker !
com.gx.kt.study.by.cls.Worker@50f8360d ,他的 workName 被设置为 Android Developer Manager !
com.gx.kt.study.by.cls.Worker@50f8360d ,他的 默认 workName 是 Android Developer Manager !

其中的参数解释如下:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;
  • property —— 必须是类型 KProperty<*>或其超类型。(类似于java 中的Field)
  • value —— 必须与属性同类型或者是它的子类型。
另一种自定义属性委托的方式

要实现属性委托,就必须要提供getValue/setValue方法,对于比较懒的同学可能就要说了,这么复杂的参数,还要每次都要手写,真是麻烦,一不小心就写错了。确实是这样,为了解决这个问题, Kotlin 标准库中声明了2个含所需 operator方法的 ReadOnlyProperty / ReadWriteProperty 接口。

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

被委托类 实现这两个接口其中之一就可以了,val 属性实现ReadOnlyProperty,var属性实现ReadOnlyProperty。 

Kotlin 标准库中提供几个委托

Kotlin 标准库中提供了几种委托,例如:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
延迟属性 by lazy

lazy() 是接受一个 lambda 并返回一个 Lazy  实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

val age : Int by lazy {
    var realAge :Int = 10
    realAge = 10 + 1
    println("计算年龄 $realAge ") // 只有第一次调用会计算,后面会缓存在age里
    realAge
}

fun main() {
    println(age)
    println(age)
}
// 计算年龄 11 
// 11
// 11

lazy() 方法 也可以接受参数 ,默认为LazyThreadSafetyMode.SYNCHRONIZED

  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode. PUBLICATION: 初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程
可观察属性 observable / vetoable

如果你要观察一个属性的变化过程,那么可以将属性委托给Delegates.observable, observable函数原型如下:

var number: Int by Delegates.observable(1){
        property, oldValue, newValue ->
       println(" 属性名  : ${property.name}: 从  $oldValue -> $newValue ")
}
fun main() {
      number = 2;
      number = 3;
}
// 属性名  : number: 从  1 -> 2
// 属性名  : number: 从  2 -> 3

相比observable,vetoable 可以控制 新值的配置是否生效。假设一个人得年龄上限最大为100。

var age: Int by Delegates.vetoable(0) {
    property, oldValue, newValue ->
    newValue <= 100  //最后一行必须是逻辑
}

fun main() {
    println(age)
    age = 2;
    println(age)
    age = 101;
    println(age)
}
// 0
// 2
// 2

可以看出,当年龄大于 100时,并未赋值成功

委托给另一个属性

一个属性可以把它的 getter 与 setter 委托给另一个属性。

  • 顶层属性 var newValue: Int by ::value

  • 同一个类的成员或扩展属性
    var newValue: Int by this::newValue

  • 另一个类的成员或扩展属性
    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt

    委托属性时,查到的结果始终是最新值
    
属性存储在映射中

还有一种情况,在一个映射(map)里存储属性的值,使用映射实例自身作为委托来实现委托属性,如:

fun main() {
    val user = User(mapOf(
        "name" to "guoxu",
        "age"  to 28
    ))
    println("name=${user.name} age=${user.age}")
}

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

注意:map的 key 和 属性名, value 和属性的数据格式 必须对应,否则报错

参考资料